Repository: microsoft/griffel
Pull request #326 (2023-01-31 15:15:46)

fix: avoid conflicts with SplitChunksPlugin
This PR fixes compatibility between our plugin and [`SplitChunksPlugin`](https://webpack.js.org/plugins/split-chunks-plugin/).

### What happened?

To process generated CSS `GriffelExtractionPlugin` required to have all CSS under `griffel` chunk. Before this PR `SplitChunksPlugin` could move out CSS from `griffel` chunk and leave unprocessed CSS.

```mermaid
graph TD
    A[GriffelExtractionPlugin] -->|Moves generated CSS to `griffel` chunk| B[SplitChunksPlugin]
    B -->|Moves out CSS from `griffel` chunk| C[GriffelExtractionPlugin fails to process all generated CSS]
```

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

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

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/change/@griffel-webpack-extraction-plugin-9ff1e7df-41e1-40ee-bc7e-6568075d0e22.json b/change/@griffel-webpack-extraction-plugin-9ff1e7df-41e1-40ee-bc7e-6568075d0e22.json
new file mode 100644
index 0000000..189618f
--- /dev/null
+++ b/change/@griffel-webpack-extraction-plugin-9ff1e7df-41e1-40ee-bc7e-6568075d0e22.json
@@ -0,0 +1,7 @@
+{
+  "type": "patch",
+  "comment": "fix: avoid conflicts with SplitChunksPlugin",
+  "packageName": "@griffel/webpack-extraction-plugin",
+  "email": "olfedias@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/chunkA.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/chunkA.ts
new file mode 100644
index 0000000..ed27c56
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/chunkA.ts
@@ -0,0 +1,10 @@
+import { __styles } from '@griffel/react';
+
+export default __styles(
+  {
+    root: { sj55zd: 'fe3e8s9' },
+  },
+  {
+    d: ['.fe3e8s9{color:red;}'],
+  },
+);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/chunkB.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/chunkB.ts
new file mode 100644
index 0000000..ed27c56
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/chunkB.ts
@@ -0,0 +1,10 @@
+import { __styles } from '@griffel/react';
+
+export default __styles(
+  {
+    root: { sj55zd: 'fe3e8s9' },
+  },
+  {
+    d: ['.fe3e8s9{color:red;}'],
+  },
+);
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/code.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/code.ts
new file mode 100644
index 0000000..554aec7
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/code.ts
@@ -0,0 +1,19 @@
+import { __styles } from '@griffel/react';
+
+export const styles = __styles(
+  {
+    root: {
+      Bi91k9c: 'faf35ka',
+    },
+  },
+  {
+    d: ['.fcnqdeg{background-color:green;}'],
+  },
+);
+
+export async function loadStyles() {
+  const stylesA = await import('./chunkA');
+  const stylesB = await import('./chunkB');
+
+  return [stylesA, stylesB];
+}
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/fs.json b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/fs.json
new file mode 100644
index 0000000..07940c6
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/fs.json
@@ -0,0 +1,1 @@
+["bundle.js", "griffel.css", "split.bundle.js"]
diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/output.css b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/output.css
new file mode 100644
index 0000000..a4f3fe2
--- /dev/null
+++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/config-split-chunks/output.css
@@ -0,0 +1,7 @@
+/** griffel.css **/
+.fcnqdeg {
+  background-color: green;
+}
+.fe3e8s9 {
+  color: red;
+}
diff --git a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
index ead2add..6c0da8e 100644
--- a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
+++ b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.ts
@@ -2,6 +2,7 @@ import { defaultCompareMediaQueries, GriffelRenderer } from '@griffel/core';
 import { Compilation } from 'webpack';
 import type { Chunk, Compiler, Module, sources } from 'webpack';
 
+import { GriffelDummyModule } from './GriffelDummyModule';
 import { parseCSSRules } from './parseCSSRules';
 import { sortCSSRules } from './sortCSSRules';
 
@@ -78,6 +79,10 @@ function ensureModuleHasPostOrderIndex(griffelChunk: Chunk, cssModule: Module) {
 
 function moveCSSModulesToGriffelChunk(compilation: Compilation, chunks: Iterable<Chunk>, griffelChunk: Chunk) {
   for (const chunk of chunks) {
+    if (chunk === griffelChunk) {
+      continue;
+    }
+
     // https://github.com/webpack-contrib/mini-css-extract-plugin/blob/26334462e419026086856787d672b052cd916c62/src/index.js#L693-L697
     const cssModules = compilation.chunkGraph.getChunkModulesIterableBySourceType(chunk, 'css/mini-extract');
 
@@ -112,10 +117,27 @@ export class GriffelCSSExtractionPlugin {
     compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
       // A chunk where we will move all CSS modules
       const griffelChunk = compilation.addChunk('griffel');
+      // Set up a dummy module to prevent the chunk from getting cleaned up by RemoveEmptyChunksPlugin
+      const dummyModule = new GriffelDummyModule();
 
-      compilation.hooks.afterChunks.tap(PLUGIN_NAME, chunks => {
+      compilation.hooks.afterChunks.tap(PLUGIN_NAME, () => {
         attachGriffelChunkToMainEntryPoint(compilation, griffelChunk);
+      });
+
+      // Adds dummy module here to try make sure that other module
+      // optimization steps won't conflict
+      compilation.hooks.afterOptimizeModules.tap(PLUGIN_NAME, () => {
+        compilation.modules.add(dummyModule);
+        compilation.chunkGraph.connectChunkAndModule(griffelChunk, dummyModule);
+      });
+
+      // Remove dummy module once we are sure chunk optimization steps
+      // have finished. i.e. won't conflict with SplitChunksPlugin
+      compilation.hooks.afterOptimizeChunks.tap(PLUGIN_NAME, chunks => {
         moveCSSModulesToGriffelChunk(compilation, chunks, griffelChunk);
+
+        compilation.modules.delete(dummyModule);
+        compilation.chunkGraph.disconnectChunkAndModule(griffelChunk, dummyModule);
       });
 
       compilation.hooks.processAssets.tap(
diff --git a/packages/webpack-extraction-plugin/src/GriffelDummyModule.ts b/packages/webpack-extraction-plugin/src/GriffelDummyModule.ts
new file mode 100644
index 0000000..2782153
--- /dev/null
+++ b/packages/webpack-extraction-plugin/src/GriffelDummyModule.ts
@@ -0,0 +1,28 @@
+import { Module } from 'webpack';
+
+const TYPES = new Set(['unknown']);
+
+export class GriffelDummyModule extends Module {
+  readonly #identifier: string;
+
+  constructor() {
+    super('unknown', undefined);
+
+    this.#identifier = 'griffel-dummy-module';
+
+    this.buildInfo = {};
+    this.buildMeta = {};
+  }
+
+  override identifier() {
+    return this.#identifier;
+  }
+
+  override getSourceTypes() {
+    return TYPES;
+  }
+
+  override size() {
+    return 0;
+  }
+}


Test patch:
diff --git a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts
index 43825c7..97ffe37 100644
--- a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts
+++ b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts
@@ -235,6 +235,24 @@ describe('webpackLoader', () => {
   // Custom filenames in mini-css-extract-plugin
   testFixture('config-name', { cssFilename: '[name].[contenthash].css' });
 
+  // Config that forces chunk splitting
+  testFixture('config-split-chunks', {
+    webpackConfig: {
+      optimization: {
+        splitChunks: {
+          cacheGroups: {
+            styles: {
+              enforce: true,
+              name: 'split',
+              test: /chunk[A|B]/,
+              chunks: 'all',
+            },
+          },
+        },
+      },
+    },
+  });
+
   // "pathinfo" adds comments with paths to output
   testFixture('basic-rules', { webpackConfig: { output: { pathinfo: true } } });