From fc9b73d761b43a248f9247c097cac094e1b58791 Mon Sep 17 00:00:00 2001
From: h-a-n-a <andywangsy@gmail.com>
Date: Wed, 10 May 2023 17:32:59 +0800
Subject: [PATCH 1/3] feat: init

feat: finish inline match resource

chore: cleanup

chore: cleanup

feat: add experimental css support

feat: dispatch plugin dynammically

feat: support option `experimentalInlineMatchResource`

chore: enable test on CI

chore: disable sourceMap generation

]

docs: add option doc

docs: correct version

fix: fix lang matching

feat: use a relatively better version test

fix: fix incorrect match with `experiments.css` enabled

fix: more accurate when matching styles

feat: optimize for `Rule.loader`

fix: import
---
 .github/workflows/ci.yml | 12 ++++++
 README.md                |  4 ++
 jest.config.js           | 10 ++++-
 package.json             |  1 +
 src/index.ts             | 85 +++++++++++++++++++++++++++++++++++-----
 src/pitcher.ts           | 66 ++++++++++++++++++++++++++++---
 src/plugin.ts            | 23 +++++++----
 src/pluginWebpack5.ts    | 57 ++++++++++++++++++++++-----
 src/util.ts              | 36 +++++++++++++++++
 test/advanced.spec.ts    | 21 +++++++---
 test/edgeCases.spec.ts   | 19 ++++-----
 test/style.spec.ts       | 11 ++++--
 test/template.spec.ts    |  4 +-
 test/utils.ts            | 49 +++++++++++++++++------
 tsconfig.json            | 10 +----
 15 files changed, 337 insertions(+), 71 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 80fba103b..368015cff 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,3 +30,15 @@ jobs:
           cache: 'yarn'
       - run: yarn install
       - run: yarn test
+
+  test-webpack5-inline-match-resource:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set node version to 16
+        uses: actions/setup-node@v2
+        with:
+          node-version: 16
+          cache: 'yarn'
+      - run: yarn install
+      - run: yarn test:match-resource
diff --git a/README.md b/README.md
index aff9518b6..b3f34888c 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,10 @@
 
 - [Documentation](https://vue-loader.vuejs.org)
 
+## v17.1+ Only Options
+
+- `experimentalInlineMatchResource: boolean`: enable [Inline matchResource](https://webpack.js.org/api/loaders/#inline-matchresource) for rule matching for vue-loader.
+
 ## v16+ Only Options
 
 - `reactivityTransform: boolean`: enable [Vue Reactivity Transform](https://github.com/vuejs/rfcs/discussions/369) (SFCs only).
diff --git a/jest.config.js b/jest.config.js
index 4f710abb7..fc6d1b325 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,4 +1,12 @@
-console.log(`running tests with webpack ${process.env.WEBPACK4 ? '4' : '5'}...`)
+const isWebpack4 = process.env.WEBPACK4
+
+console.log(
+  `running tests with webpack ${isWebpack4 ? '4' : '5'}${
+    !isWebpack4 && process.env.INLINE_MATCH_RESOURCE
+      ? ' with inline match resource enabled'
+      : ''
+  }...`
+)
 
 module.exports = {
   preset: 'ts-jest',
diff --git a/package.json b/package.json
index bf2d9a177..c2025af72 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "build": "tsc",
     "pretest": "tsc",
     "test": "jest",
+    "test:match-resource": "INLINE_MATCH_RESOURCE=true jest",
     "pretest:webpack4": "tsc",
     "test:webpack4": "WEBPACK4=true jest",
     "dev-example": "node example/devServer.js --config example/webpack.config.js --inline --hot",
diff --git a/src/index.ts b/src/index.ts
index aa4dfc9df..21be9e5c4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -20,7 +20,12 @@ import { formatError } from './formatError'
 import VueLoaderPlugin from './plugin'
 import { canInlineTemplate } from './resolveScript'
 import { setDescriptor } from './descriptorCache'
-import { getOptions, stringifyRequest as _stringifyRequest } from './util'
+import {
+  getOptions,
+  stringifyRequest as _stringifyRequest,
+  genMatchResource,
+  testWebpack5,
+} from './util'
 
 export { VueLoaderPlugin }
 
@@ -51,6 +56,7 @@ export interface VueLoaderOptions {
   exposeFilename?: boolean
   appendExtension?: boolean
   enableTsInTemplate?: boolean
+  experimentalInlineMatchResource?: boolean
 
   isServerBuild?: boolean
 }
@@ -90,18 +96,23 @@ export default function loader(
     rootContext,
     resourcePath,
     resourceQuery: _resourceQuery = '',
+    _compiler,
   } = loaderContext
 
+  const isWebpack5 = testWebpack5(_compiler)
   const rawQuery = _resourceQuery.slice(1)
   const incomingQuery = qs.parse(rawQuery)
   const resourceQuery = rawQuery ? `&${rawQuery}` : ''
   const options = (getOptions(loaderContext) || {}) as VueLoaderOptions
+  const enableInlineMatchResource =
+    isWebpack5 && Boolean(options.experimentalInlineMatchResource)
 
   const isServer = options.isServerBuild ?? target === 'node'
   const isProduction =
     mode === 'production' || process.env.NODE_ENV === 'production'
 
   const filename = resourcePath.replace(/\?.*$/, '')
+
   const { descriptor, errors } = parse(source, {
     filename,
     sourceMap,
@@ -167,10 +178,23 @@ export default function loader(
   if (script || scriptSetup) {
     const lang = script?.lang || scriptSetup?.lang
     isTS = !!(lang && /tsx?/.test(lang))
+    const externalQuery = Boolean(script && !scriptSetup && script.src)
+      ? `&external`
+      : ``
     const src = (script && !scriptSetup && script.src) || resourcePath
     const attrsQuery = attrsToQuery((scriptSetup || script)!.attrs, 'js')
-    const query = `?vue&type=script${attrsQuery}${resourceQuery}`
-    const scriptRequest = stringifyRequest(src + query)
+    const query = `?vue&type=script${attrsQuery}${resourceQuery}${externalQuery}`
+
+    let scriptRequest: string
+
+    if (enableInlineMatchResource) {
+      scriptRequest = stringifyRequest(
+        genMatchResource(this, src, query, lang || 'js')
+      )
+    } else {
+      scriptRequest = stringifyRequest(src + query)
+    }
+
     scriptImport =
       `import script from ${scriptRequest}\n` +
       // support named exports
@@ -184,13 +208,27 @@ export default function loader(
   const useInlineTemplate = canInlineTemplate(descriptor, isProduction)
   if (descriptor.template && !useInlineTemplate) {
     const src = descriptor.template.src || resourcePath
+    const externalQuery = Boolean(descriptor.template.src) ? `&external` : ``
     const idQuery = `&id=${id}`
     const scopedQuery = hasScoped ? `&scoped=true` : ``
     const attrsQuery = attrsToQuery(descriptor.template.attrs)
     const tsQuery =
       options.enableTsInTemplate !== false && isTS ? `&ts=true` : ``
-    const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}`
-    templateRequest = stringifyRequest(src + query)
+    const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}${externalQuery}`
+
+    if (enableInlineMatchResource) {
+      templateRequest = stringifyRequest(
+        genMatchResource(
+          this,
+          src,
+          query,
+          options.enableTsInTemplate !== false && isTS ? 'ts' : 'js'
+        )
+      )
+    } else {
+      templateRequest = stringifyRequest(src + query)
+    }
+
     templateImport = `import { ${renderFnName} } from ${templateRequest}`
     propsToAttach.push([renderFnName, renderFnName])
   }
@@ -205,12 +243,23 @@ export default function loader(
       .forEach((style, i) => {
         const src = style.src || resourcePath
         const attrsQuery = attrsToQuery(style.attrs, 'css')
+        const lang = String(style.attrs.lang || 'css')
         // make sure to only pass id when necessary so that we don't inject
         // duplicate tags when multiple components import the same css file
         const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
         const inlineQuery = asCustomElement ? `&inline` : ``
-        const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}`
-        const styleRequest = stringifyRequest(src + query)
+        const externalQuery = Boolean(style.src) ? `&external` : ``
+        const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}${externalQuery}`
+
+        let styleRequest
+        if (enableInlineMatchResource) {
+          styleRequest = stringifyRequest(
+            genMatchResource(this, src, query, lang)
+          )
+        } else {
+          styleRequest = stringifyRequest(src + query)
+        }
+
         if (style.module) {
           if (asCustomElement) {
             loaderContext.emitError(
@@ -283,9 +332,27 @@ export default function loader(
           const issuerQuery = block.attrs.src
             ? `&issuerPath=${qs.escape(resourcePath)}`
             : ''
-          const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}`
+
+          const externalQuery = Boolean(block.attrs.src) ? `&external` : ``
+          const query = `?vue&type=custom&index=${i}${blockTypeQuery}${issuerQuery}${attrsQuery}${resourceQuery}${externalQuery}`
+
+          let customRequest
+
+          if (enableInlineMatchResource) {
+            customRequest = stringifyRequest(
+              genMatchResource(
+                this,
+                src as string,
+                query,
+                block.attrs.lang as string
+              )
+            )
+          } else {
+            customRequest = stringifyRequest(src + query)
+          }
+
           return (
-            `import block${i} from ${stringifyRequest(src + query)}\n` +
+            `import block${i} from ${customRequest}\n` +
             `if (typeof block${i} === 'function') block${i}(script)`
           )
         })
diff --git a/src/pitcher.ts b/src/pitcher.ts
index e90acc9a5..f12f5405a 100644
--- a/src/pitcher.ts
+++ b/src/pitcher.ts
@@ -1,6 +1,6 @@
 import type { LoaderDefinitionFunction, LoaderContext } from 'webpack'
 import * as qs from 'querystring'
-import { stringifyRequest } from './util'
+import { getOptions, stringifyRequest, testWebpack5 } from './util'
 import { VueLoaderOptions } from '.'
 
 const selfPath = require.resolve('./index')
@@ -58,7 +58,40 @@ export const pitch = function () {
   })
 
   // Inject style-post-loader before css-loader for scoped CSS and trimming
+  const isWebpack5 = testWebpack5(context._compiler)
+  const options = (getOptions(context) || {}) as VueLoaderOptions
   if (query.type === `style`) {
+    if (isWebpack5 && context._compiler?.options.experiments.css) {
+      // If user enables `experiments.css`, then we are trying to emit css code directly.
+      // Although we can target requests like `xxx.vue?type=style` to match `type: "css"`,
+      // it will make the plugin a mess.
+      if (!options.experimentalInlineMatchResource) {
+        context.emitError(
+          new Error(
+            '`experimentalInlineMatchResource` should be enabled if `experiments.css` enabled currently'
+          )
+        )
+        return ''
+      }
+
+      if (query.inline || query.module) {
+        context.emitError(
+          new Error(
+            '`inline` or `module` is currently not supported with `experiments.css` enabled'
+          )
+        )
+        return ''
+      }
+
+      const loaderString = [stylePostLoaderPath, ...loaders]
+        .map((loader) => {
+          return typeof loader === 'string' ? loader : loader.request
+        })
+        .join('!')
+      return `@import "${context.resourcePath}${
+        query.lang ? `.${query.lang}` : ''
+      }${context.resourceQuery}!=!-!${loaderString}!${context.resource}";`
+    }
     const cssLoaderIndex = loaders.findIndex(isCSSLoader)
     if (cssLoaderIndex > -1) {
       // if inlined, ignore any loaders after css-loader and replace w/ inline
@@ -71,7 +104,8 @@ export const pitch = function () {
       return genProxyModule(
         [...afterLoaders, stylePostLoaderPath, ...beforeLoaders],
         context,
-        !!query.module || query.inline != null
+        !!query.module || query.inline != null,
+        (query.lang as string) || 'css'
       )
     }
   }
@@ -84,15 +118,21 @@ export const pitch = function () {
 
   // Rewrite request. Technically this should only be done when we have deduped
   // loaders. But somehow this is required for block source maps to work.
-  return genProxyModule(loaders, context, query.type !== 'template')
+  return genProxyModule(
+    loaders,
+    context,
+    query.type !== 'template',
+    query.ts ? 'ts' : (query.lang as string)
+  )
 }
 
 function genProxyModule(
   loaders: (Loader | string)[],
   context: LoaderContext<VueLoaderOptions>,
-  exportDefault = true
+  exportDefault = true,
+  lang = 'js'
 ) {
-  const request = genRequest(loaders, context)
+  const request = genRequest(loaders, lang, context)
   // return a proxy module which simply re-exports everything from the
   // actual request. Note for template blocks the compiled module has no
   // default export.
@@ -104,12 +144,28 @@ function genProxyModule(
 
 function genRequest(
   loaders: (Loader | string)[],
+  lang: string,
   context: LoaderContext<VueLoaderOptions>
 ) {
+  const isWebpack5 = testWebpack5(context._compiler)
+  const options = (getOptions(context) || {}) as VueLoaderOptions
+  const enableInlineMatchResource =
+    isWebpack5 && options.experimentalInlineMatchResource
+
   const loaderStrings = loaders.map((loader) => {
     return typeof loader === 'string' ? loader : loader.request
   })
   const resource = context.resourcePath + context.resourceQuery
+
+  if (enableInlineMatchResource) {
+    return stringifyRequest(
+      context,
+      `${context.resourcePath}${lang ? `.${lang}` : ''}${
+        context.resourceQuery
+      }!=!-!${[...loaderStrings, resource].join('!')}`
+    )
+  }
+
   return stringifyRequest(
     context,
     '-!' + [...loaderStrings, resource].join('!')
diff --git a/src/plugin.ts b/src/plugin.ts
index a7b4a42d4..72caaadce 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -1,19 +1,26 @@
-import webpack from 'webpack'
 import type { Compiler } from 'webpack'
+import { testWebpack5 } from './util'
 
 declare class VueLoaderPlugin {
   static NS: string
   apply(compiler: Compiler): void
 }
 
-let Plugin: typeof VueLoaderPlugin
+const NS = 'vue-loader'
 
-if (webpack.version && webpack.version[0] > '4') {
-  // webpack5 and upper
-  Plugin = require('./pluginWebpack5').default
-} else {
-  // webpack4 and lower
-  Plugin = require('./pluginWebpack4').default
+class Plugin {
+  static NS = NS
+  apply(compiler: Compiler) {
+    let Ctor: typeof VueLoaderPlugin
+    if (testWebpack5(compiler)) {
+      // webpack5 and upper
+      Ctor = require('./pluginWebpack5').default
+    } else {
+      // webpack4 and lower
+      Ctor = require('./pluginWebpack4').default
+    }
+    new Ctor().apply(compiler)
+  }
 }
 
 export default Plugin
diff --git a/src/pluginWebpack5.ts b/src/pluginWebpack5.ts
index 75152851b..649cd54c7 100644
--- a/src/pluginWebpack5.ts
+++ b/src/pluginWebpack5.ts
@@ -1,6 +1,6 @@
 import * as qs from 'querystring'
 import type { VueLoaderOptions } from './'
-import type { RuleSetRule, Compiler } from 'webpack'
+import type { RuleSetRule, Compiler, RuleSetUse } from 'webpack'
 import { needHMR } from './util'
 import { clientCache, typeDepToSFCMap } from './resolveScript'
 import { compiler as vueCompiler } from './compiler'
@@ -146,7 +146,7 @@ class VueLoaderPlugin {
       )
     }
 
-    // get the normlized "use" for vue files
+    // get the normalized "use" for vue files
     const vueUse = vueRules
       .filter((rule) => rule.type === 'use')
       .map((rule) => rule.value)
@@ -170,6 +170,8 @@ class VueLoaderPlugin {
     const vueLoaderUse = vueUse[vueLoaderUseIndex]
     const vueLoaderOptions = (vueLoaderUse.options =
       vueLoaderUse.options || {}) as VueLoaderOptions
+    const enableInlineMatchResource =
+      vueLoaderOptions.experimentalInlineMatchResource
 
     // for each user rule (except the vue rule), create a cloned rule
     // that targets the corresponding language blocks in *.vue files.
@@ -221,16 +223,53 @@ class VueLoaderPlugin {
         const parsed = qs.parse(query.slice(1))
         return parsed.vue != null
       },
+      options: vueLoaderOptions,
     }
 
     // replace original rules
-    compiler.options.module!.rules = [
-      pitcher,
-      ...jsRulesForRenderFn,
-      templateCompilerRule,
-      ...clonedRules,
-      ...rules,
-    ]
+    if (enableInlineMatchResource) {
+      // Match rules using `vue-loader`
+      const vueLoaderRules = rules.filter((rule) => {
+        const matchOnce = (use?: RuleSetUse) => {
+          let loaderString = ''
+
+          if (!use) {
+            return loaderString
+          }
+
+          if (typeof use === 'string') {
+            loaderString = use
+          } else if (Array.isArray(use)) {
+            loaderString = matchOnce(use[0])
+          } else if (typeof use === 'object' && use.loader) {
+            loaderString = use.loader
+          }
+          return loaderString
+        }
+
+        const loader = rule.loader || matchOnce(rule.use)
+        return (
+          loader === require('../package.json').name ||
+          loader.startsWith(require.resolve('./index'))
+        )
+      })
+
+      compiler.options.module!.rules = [
+        pitcher,
+        ...rules.filter((rule) => !vueLoaderRules.includes(rule)),
+        templateCompilerRule,
+        ...clonedRules,
+        ...vueLoaderRules,
+      ]
+    } else {
+      compiler.options.module!.rules = [
+        pitcher,
+        ...jsRulesForRenderFn,
+        templateCompilerRule,
+        ...clonedRules,
+        ...rules,
+      ]
+    }
 
     // 3.3 HMR support for imported types
     if (
diff --git a/src/util.ts b/src/util.ts
index 1a2718fac..afef3c8ed 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,4 +1,5 @@
 import type { Compiler, LoaderContext } from 'webpack'
+import qs from 'querystring'
 import type { SFCDescriptor, CompilerOptions } from 'vue/compiler-sfc'
 import type { VueLoaderOptions } from '.'
 import * as path from 'path'
@@ -163,3 +164,38 @@ export function stringifyRequest(
       .join('!')
   )
 }
+
+export function genMatchResource(
+  context: LoaderContext<VueLoaderOptions>,
+  resourcePath: string,
+  resourceQuery?: string,
+  lang?: string,
+  additionalLoaders?: string[]
+) {
+  resourceQuery = resourceQuery || ''
+  additionalLoaders = additionalLoaders || []
+
+  const loaders = [...additionalLoaders]
+  const parsedQuery = qs.parse(resourceQuery.slice(1))
+
+  // process non-external resources
+  if ('vue' in parsedQuery && !('external' in parsedQuery)) {
+    const currentRequest = context.loaders
+      .slice(context.loaderIndex)
+      .map((obj) => obj.request)
+    loaders.push(...currentRequest)
+  }
+  const loaderString = loaders.join('!')
+
+  return `${resourcePath}${lang ? `.${lang}` : ''}${resourceQuery}!=!${
+    loaderString ? `${loaderString}!` : ''
+  }${resourcePath}${resourceQuery}`
+}
+
+export const testWebpack5 = (compiler?: Compiler) => {
+  if (!compiler) {
+    return false
+  }
+  const webpackVersion = compiler?.webpack?.version
+  return Boolean(webpackVersion && Number(webpackVersion.split('.')[0]) > 4)
+}
diff --git a/test/advanced.spec.ts b/test/advanced.spec.ts
index 9fee7e833..eec28bc95 100644
--- a/test/advanced.spec.ts
+++ b/test/advanced.spec.ts
@@ -1,6 +1,12 @@
 import { SourceMapConsumer } from 'source-map'
 import { fs as mfs } from 'memfs'
-import { bundle, mockBundleAndRun, normalizeNewline, genId } from './utils'
+import {
+  bundle,
+  mockBundleAndRun,
+  normalizeNewline,
+  genId,
+  DEFAULT_VUE_USE,
+} from './utils'
 
 const MiniCssExtractPlugin = require('mini-css-extract-plugin')
 
@@ -10,7 +16,7 @@ test('support chaining with other loaders', async () => {
     modify: (config) => {
       config!.module!.rules[0] = {
         test: /\.vue$/,
-        use: ['vue-loader', require.resolve('./mock-loaders/js')],
+        use: [DEFAULT_VUE_USE, require.resolve('./mock-loaders/js')],
       }
     },
   })
@@ -24,7 +30,7 @@ test.skip('inherit queries on files', async () => {
     modify: (config) => {
       config!.module!.rules[0] = {
         test: /\.vue$/,
-        use: ['vue-loader', require.resolve('./mock-loaders/query')],
+        use: [DEFAULT_VUE_USE, require.resolve('./mock-loaders/query')],
       }
     },
   })
@@ -92,7 +98,7 @@ test('extract CSS', async () => {
       config.module.rules = [
         {
           test: /\.vue$/,
-          use: 'vue-loader',
+          use: [DEFAULT_VUE_USE],
         },
         {
           test: /\.css$/,
@@ -126,7 +132,7 @@ test('extract CSS with code spliting', async () => {
       config.module.rules = [
         {
           test: /\.vue$/,
-          use: 'vue-loader',
+          use: [DEFAULT_VUE_USE],
         },
         {
           test: /\.css$/,
@@ -153,7 +159,10 @@ test('support rules with oneOf', async () => {
       entry,
       modify: (config: any) => {
         config!.module!.rules = [
-          { test: /\.vue$/, loader: 'vue-loader' },
+          {
+            test: /\.vue$/,
+            use: [DEFAULT_VUE_USE],
+          },
           {
             test: /\.css$/,
             use: 'style-loader',
diff --git a/test/edgeCases.spec.ts b/test/edgeCases.spec.ts
index d5394b3d1..a0c5aadfc 100644
--- a/test/edgeCases.spec.ts
+++ b/test/edgeCases.spec.ts
@@ -1,6 +1,12 @@
 import * as path from 'path'
 import webpack from 'webpack'
-import { mfs, bundle, mockBundleAndRun, normalizeNewline } from './utils'
+import {
+  mfs,
+  bundle,
+  mockBundleAndRun,
+  normalizeNewline,
+  DEFAULT_VUE_USE,
+} from './utils'
 
 // @ts-ignore
 function assertComponent({
@@ -37,7 +43,7 @@ test('vue rule with include', async () => {
       config.module.rules[i] = {
         test: /\.vue$/,
         include: /fixtures/,
-        loader: 'vue-loader',
+        use: [DEFAULT_VUE_USE],
       }
     },
   })
@@ -52,7 +58,7 @@ test('test-less oneOf rules', async () => {
       config!.module!.rules = [
         {
           test: /\.vue$/,
-          loader: 'vue-loader',
+          use: [DEFAULT_VUE_USE],
         },
         {
           oneOf: [
@@ -79,12 +85,7 @@ test('normalize multiple use + options', async () => {
       )
       config!.module!.rules[i] = {
         test: /\.vue$/,
-        use: [
-          {
-            loader: 'vue-loader',
-            options: {},
-          },
-        ],
+        use: [DEFAULT_VUE_USE],
       }
     },
   })
diff --git a/test/style.spec.ts b/test/style.spec.ts
index 94a04eb0e..4bb998dbe 100644
--- a/test/style.spec.ts
+++ b/test/style.spec.ts
@@ -1,4 +1,9 @@
-import { mockBundleAndRun, genId, normalizeNewline } from './utils'
+import {
+  mockBundleAndRun,
+  genId,
+  normalizeNewline,
+  DEFAULT_VUE_USE,
+} from './utils'
 
 test('scoped style', async () => {
   const { window, instance, componentModule } = await mockBundleAndRun({
@@ -109,7 +114,7 @@ test('CSS Modules', async () => {
         config!.module!.rules = [
           {
             test: /\.vue$/,
-            loader: 'vue-loader',
+            use: [DEFAULT_VUE_USE],
           },
           {
             test: /\.css$/,
@@ -178,7 +183,7 @@ test('CSS Modules Extend', async () => {
       config!.module!.rules = [
         {
           test: /\.vue$/,
-          loader: 'vue-loader',
+          use: [DEFAULT_VUE_USE],
         },
         {
           test: /\.css$/,
diff --git a/test/template.spec.ts b/test/template.spec.ts
index 52eda51cb..e46758a54 100644
--- a/test/template.spec.ts
+++ b/test/template.spec.ts
@@ -1,5 +1,5 @@
 import * as path from 'path'
-import { mockBundleAndRun, normalizeNewline } from './utils'
+import { DEFAULT_VUE_USE, mockBundleAndRun, normalizeNewline } from './utils'
 
 test('apply babel transformations to expressions in template', async () => {
   const { instance } = await mockBundleAndRun({
@@ -111,7 +111,7 @@ test('should allow process custom file', async () => {
       rules: [
         {
           test: /\.svg$/,
-          loader: 'vue-loader',
+          use: [DEFAULT_VUE_USE],
         },
       ],
     },
diff --git a/test/utils.ts b/test/utils.ts
index 628d24da8..6ac719ea0 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -6,8 +6,14 @@ import hash from 'hash-sum'
 // import MiniCssExtractPlugin from 'mini-css-extract-plugin'
 import { fs as mfs } from 'memfs'
 import { JSDOM, VirtualConsole } from 'jsdom'
-import { VueLoaderPlugin } from '..'
-import type { VueLoaderOptions } from '..'
+import type { VueLoaderOptions, VueLoaderPlugin } from '..'
+
+export const DEFAULT_VUE_USE = {
+  loader: 'vue-loader',
+  options: {
+    experimentalInlineMatchResource: Boolean(process.env.INLINE_MATCH_RESOURCE),
+  },
+}
 
 const baseConfig: webpack.Configuration = {
   mode: 'development',
@@ -29,11 +35,7 @@ const baseConfig: webpack.Configuration = {
     rules: [
       {
         test: /\.vue$/,
-        loader: 'vue-loader',
-      },
-      {
-        test: /\.css$/,
-        use: ['style-loader', 'css-loader'],
+        use: [DEFAULT_VUE_USE],
       },
       {
         test: /\.ts$/,
@@ -73,16 +75,41 @@ export function bundle(
 }> {
   let config: BundleOptions = merge({}, baseConfig, options)
 
+  if (!options.experiments?.css) {
+    config.module?.rules?.push({
+      test: /\.css$/,
+      use: ['style-loader', 'css-loader'],
+    })
+  }
+
   if (config.vue && config.module) {
-    const vueOptions = options.vue
+    const vueOptions = {
+      // Test experimental inline match resource by default
+      experimentalInlineMatchResource: Boolean(
+        process.env.INLINE_MATCH_RESOURCE
+      ),
+      ...options.vue,
+    }
+
     delete config.vue
     const vueIndex = config.module.rules!.findIndex(
       (r: any) => r.test instanceof RegExp && r.test.test('.vue')
     )
     const vueRule = config.module.rules![vueIndex]
-    config.module.rules![vueIndex] = Object.assign({}, vueRule, {
-      options: vueOptions,
-    })
+
+    // Detect `Rule.use` or `Rule.loader` and `Rule.options` combination
+    if (vueRule && typeof vueRule === 'object' && Array.isArray(vueRule.use)) {
+      // Vue usually locates at the first loader
+      if (typeof vueRule.use?.[0] === 'object') {
+        vueRule.use[0] = Object.assign({}, vueRule.use[0], {
+          options: vueOptions,
+        })
+      }
+    } else {
+      config.module.rules![vueIndex] = Object.assign({}, vueRule, {
+        options: vueOptions,
+      })
+    }
   }
 
   if (typeof config.entry === 'string' && /\.vue/.test(config.entry)) {
diff --git a/tsconfig.json b/tsconfig.json
index 15b7fee10..aead67b91 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,13 +14,7 @@
     "noImplicitAny": true,
     "removeComments": false,
     "skipLibCheck": true,
-    "lib": [
-      "es6",
-      "es7",
-      "DOM"
-    ]
+    "lib": ["es6", "es7", "DOM"]
   },
-  "include": [
-    "src"
-  ]
+  "include": ["src"]
 }

From c1eef02cd18cb976140d352ecd4458176e54aefd Mon Sep 17 00:00:00 2001
From: Hana <andywangsy@gmail.com>
Date: Mon, 29 May 2023 14:48:28 +0800
Subject: [PATCH 2/3] chore: fix test

---
 package.json  | 1 +
 test/utils.ts | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c2025af72..7fd8ca6cd 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "build": "tsc",
     "pretest": "tsc",
     "test": "jest",
+    "pretest:match-resource": "tsc",
     "test:match-resource": "INLINE_MATCH_RESOURCE=true jest",
     "pretest:webpack4": "tsc",
     "test:webpack4": "WEBPACK4=true jest",
diff --git a/test/utils.ts b/test/utils.ts
index 6ac719ea0..0214fd4c9 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -6,7 +6,8 @@ import hash from 'hash-sum'
 // import MiniCssExtractPlugin from 'mini-css-extract-plugin'
 import { fs as mfs } from 'memfs'
 import { JSDOM, VirtualConsole } from 'jsdom'
-import type { VueLoaderOptions, VueLoaderPlugin } from '..'
+import { VueLoaderPlugin } from '..'
+import type { VueLoaderOptions } from '..'
 
 export const DEFAULT_VUE_USE = {
   loader: 'vue-loader',

From 9907ca65065e3c7b977b5f0895d195ea581cc55a Mon Sep 17 00:00:00 2001
From: Hana <andywangsy@gmail.com>
Date: Thu, 1 Jun 2023 14:03:32 +0800
Subject: [PATCH 3/3] chore: remove unnecessary code

---
 src/util.ts | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/util.ts b/src/util.ts
index afef3c8ed..998f14011 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -169,13 +169,11 @@ export function genMatchResource(
   context: LoaderContext<VueLoaderOptions>,
   resourcePath: string,
   resourceQuery?: string,
-  lang?: string,
-  additionalLoaders?: string[]
+  lang?: string
 ) {
   resourceQuery = resourceQuery || ''
-  additionalLoaders = additionalLoaders || []
 
-  const loaders = [...additionalLoaders]
+  const loaders: string[] = []
   const parsedQuery = qs.parse(resourceQuery.slice(1))
 
   // process non-external resources