From 3ed63fabc7add22b0c50bd4886af3521a36c6aff Mon Sep 17 00:00:00 2001
From: xc2 <xc2@users.noreply.github.com>
Date: Mon, 15 Apr 2024 21:41:40 +0800
Subject: [PATCH] fix: loaders for styles are not deduplicated when
 experiments.css is true

---
 lib/loaders/pitcher.js | 42 ++++++++++++++++++++-----------------
 package.json           |  3 +++
 test/style.spec.js     | 47 +++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 72 insertions(+), 20 deletions(-)

diff --git a/lib/loaders/pitcher.js b/lib/loaders/pitcher.js
index 5989dbe37..2e432c65f 100644
--- a/lib/loaders/pitcher.js
+++ b/lib/loaders/pitcher.js
@@ -80,30 +80,34 @@ module.exports.pitch = function (remainingRequest) {
     return
   }
 
+  // Important: dedupe since both the original rule
+  // and the cloned rule would match a source import request.
+  // also make sure to dedupe based on loader path.
+  // assumes you'd probably never want to apply the same loader on the same
+  // file twice.
+  // Exception: in Vue CLI we do need two instances of postcss-loader
+  // for user config and inline minification. So we need to dedupe baesd on
+  // path AND query to be safe.
+  const loadersSeen = new Set()
+  loaders = loaders.filter((loader) => {
+    const identifier =
+        typeof loader === 'string' ? loader : loader.path + loader.query
+    if (!loadersSeen.has(identifier)) {
+      loadersSeen.add(identifier)
+      return true
+    }
+    return false
+  })
+
   const genRequest = (loaders, lang) => {
-    // Important: dedupe since both the original rule
-    // and the cloned rule would match a source import request.
-    // also make sure to dedupe based on loader path.
-    // assumes you'd probably never want to apply the same loader on the same
-    // file twice.
-    // Exception: in Vue CLI we do need two instances of postcss-loader
-    // for user config and inline minification. So we need to dedupe baesd on
-    // path AND query to be safe.
-    const seen = new Map()
-    const loaderStrings = []
     const enableInlineMatchResource =
       isWebpack5 && options.experimentalInlineMatchResource
 
-    loaders.forEach((loader) => {
-      const identifier =
-        typeof loader === 'string' ? loader : loader.path + loader.query
+    const loaderStrings = loaders.map((loader) => {
       const request = typeof loader === 'string' ? loader : loader.request
-      if (!seen.has(identifier)) {
-        seen.set(identifier, true)
-        // loader.request contains both the resolved loader path and its options
-        // query (e.g. ??ref-0)
-        loaderStrings.push(request)
-      }
+      // loader.request contains both the resolved loader path and its options
+      // query (e.g. ??ref-0)
+      return request
     })
     if (enableInlineMatchResource) {
       return loaderUtils.stringifyRequest(
diff --git a/package.json b/package.json
index aa8d57558..9125021bf 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,9 @@
     },
     "prettier": {
       "optional": true
+    },
+    "webpack": {
+      "optional": true
     }
   },
   "dependencies": {
diff --git a/test/style.spec.js b/test/style.spec.js
index 0e35e1a78..f81efd5b5 100644
--- a/test/style.spec.js
+++ b/test/style.spec.js
@@ -3,7 +3,8 @@ const {
   genId,
   mockRender,
   mockBundleAndRun,
-  DEFAULT_VUE_USE
+  DEFAULT_VUE_USE,
+  bundle
 } = require('./utils')
 
 test('scoped style', done => {
@@ -224,3 +225,47 @@ test('CSS Modules Extend', async () => {
     })
   })
 })
+
+const webpack5Test = /^5\./.test((require('webpack').version || '')) ? test : test.skip
+
+webpack5Test('loaders should also be deduplicated when experiments.css is true', (done) => {
+  let loaders = []
+  bundle({
+    entry: 'extract-css.vue',
+    vue: { experimentalInlineMatchResource: true },
+    experiments: { css: true },
+    module: {
+      rules: [
+        {
+          test: /\.stylus$/,
+          use: ['stylus-loader'],
+          type: 'css'
+        }
+      ]
+    },
+    plugins: [
+      {
+        apply(compiler) {
+          compiler.hooks.thisCompilation.tap('a', (compilation) => {
+            compilation.hooks.buildModule.tap('a', (module) => {
+              if (/[?&]lang=stylus/.test(module.resource)) {
+                const isPitch = module.loaders.some(item => /pitcher/.test(item.loader))
+                if (isPitch) {
+                  return
+                }
+                // loaders for stylus after pitch
+                loaders = [...module.loaders]
+              }
+            })
+          })
+        }
+      }
+    ]
+  }, () => {
+    const stylusLoaderCount = loaders.filter(item => /stylus-loader/.test(item.loader)).length
+    expect(stylusLoaderCount).toBe(1)
+
+    done()
+  })
+})
+