diff --git a/packages/playground/css/__tests__/css.spec.ts b/packages/playground/css/__tests__/css.spec.ts index c37c52eabdf52a..360e46dbbba150 100644 --- a/packages/playground/css/__tests__/css.spec.ts +++ b/packages/playground/css/__tests__/css.spec.ts @@ -364,3 +364,8 @@ test('minify css', async () => { expect(cssFile).toMatch('rgba(') expect(cssFile).not.toMatch('#ffff00b3') }) + +test('import css in less', async () => { + expect(await getColor('.css-in-less')).toBe('yellow') + expect(await getColor('.css-in-less-2')).toBe('blue') +}) diff --git a/packages/playground/css/index.html b/packages/playground/css/index.html index 4de35d66441bee..acbbe44a7f8a60 100644 --- a/packages/playground/css/index.html +++ b/packages/playground/css/index.html @@ -105,6 +105,18 @@

CSS

Inlined import - this should NOT be red.

+ +
+ test import css in less, this color will be yellow +
+
+ test for import less in less, this color will be blue +
+ +
+ test import css in scss, this color will be orange +
+

 
 
diff --git a/packages/playground/css/less.less b/packages/playground/css/less.less
index f8870e06f3a72c..69ffa830862014 100644
--- a/packages/playground/css/less.less
+++ b/packages/playground/css/less.less
@@ -1,4 +1,5 @@
 @import '@/nested/nested';
+@import './nested/css-in-less.less';
 
 @color: blue;
 
diff --git a/packages/playground/css/nested/_index.scss b/packages/playground/css/nested/_index.scss
index 6f2103c79fc2c8..48d630b573ae1b 100644
--- a/packages/playground/css/nested/_index.scss
+++ b/packages/playground/css/nested/_index.scss
@@ -1,3 +1,5 @@
+@import './css-in-scss.css';
+
 .sass-at-import {
   color: olive;
   background: url(./icon.png) 10px no-repeat;
diff --git a/packages/playground/css/nested/css-in-less-2.less b/packages/playground/css/nested/css-in-less-2.less
new file mode 100644
index 00000000000000..443d17da34c0da
--- /dev/null
+++ b/packages/playground/css/nested/css-in-less-2.less
@@ -0,0 +1,3 @@
+.css-in-less-2 {
+  color: blue;
+}
diff --git a/packages/playground/css/nested/css-in-less.css b/packages/playground/css/nested/css-in-less.css
new file mode 100644
index 00000000000000..b174a601b1356c
--- /dev/null
+++ b/packages/playground/css/nested/css-in-less.css
@@ -0,0 +1,3 @@
+.css-in-less {
+  color: yellow;
+}
diff --git a/packages/playground/css/nested/css-in-less.less b/packages/playground/css/nested/css-in-less.less
new file mode 100644
index 00000000000000..abdd904b43016a
--- /dev/null
+++ b/packages/playground/css/nested/css-in-less.less
@@ -0,0 +1,4 @@
+@import url('./css-in-less.css');
+@import './css-in-less.css';
+
+@import './css-in-less-2.less';
diff --git a/packages/playground/css/nested/css-in-scss.css b/packages/playground/css/nested/css-in-scss.css
new file mode 100644
index 00000000000000..a63e49e4d6a1fd
--- /dev/null
+++ b/packages/playground/css/nested/css-in-scss.css
@@ -0,0 +1,3 @@
+.css-in-scss {
+  color: orange;
+}
diff --git a/packages/playground/css/vite.config.js b/packages/playground/css/vite.config.js
index e4dc8d5a9f265f..53d001d8387989 100644
--- a/packages/playground/css/vite.config.js
+++ b/packages/playground/css/vite.config.js
@@ -1,4 +1,5 @@
 const path = require('path')
+
 /**
  * @type {import('vite').UserConfig}
  */
diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts
index cb62b785732e0d..780a82c900b9dd 100644
--- a/packages/vite/src/node/plugins/css.ts
+++ b/packages/vite/src/node/plugins/css.ts
@@ -658,12 +658,14 @@ async function compileCSS(
     }
     // important: set this for relative import resolving
     opts.filename = cleanUrl(id)
+
     const preprocessResult = await preProcessor(
       code,
       config.root,
       opts,
       atImportResolvers
     )
+
     if (preprocessResult.errors.length) {
       throw preprocessResult.errors[0]
     }
@@ -694,10 +696,12 @@ async function compileCSS(
           if (isHTMLProxy && publicFile) {
             return publicFile
           }
+
           const resolved = await atImportResolvers.css(
             id,
             path.join(basedir, '*')
           )
+
           if (resolved) {
             return path.resolve(resolved)
           }
@@ -858,6 +862,7 @@ type CssUrlReplacer = (
 // https://drafts.csswg.org/css-syntax-3/#identifier-code-point
 export const cssUrlRE =
   /(?<=^|[^\w\-\u0080-\uffff])url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/
+export const importCssRE = /@import ('[^']+\.css'|"[^"]+\.css"|[^'")]+\.css)/
 const cssImageSetRE = /image-set\(([^)]+)\)/
 
 const UrlRewritePostcssPlugin: Postcss.PluginCreator<{
@@ -907,6 +912,16 @@ function rewriteCssUrls(
   })
 }
 
+function rewriteImportCss(
+  css: string,
+  replacer: CssUrlReplacer
+): Promise {
+  return asyncReplace(css, importCssRE, async (match) => {
+    const [matched, rawUrl] = match
+    return await doImportCSSReplace(rawUrl, matched, replacer)
+  })
+}
+
 function rewriteCssImageSet(
   css: string,
   replacer: CssUrlReplacer
@@ -937,6 +952,24 @@ async function doUrlReplace(
   return `url(${wrap}${await replacer(rawUrl)}${wrap})`
 }
 
+async function doImportCSSReplace(
+  rawUrl: string,
+  matched: string,
+  replacer: CssUrlReplacer
+) {
+  let wrap = ''
+  const first = rawUrl[0]
+  if (first === `"` || first === `'`) {
+    wrap = first
+    rawUrl = rawUrl.slice(1, -1)
+  }
+  if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) {
+    return matched
+  }
+
+  return `@import ${wrap}${await replacer(rawUrl)}${wrap}`
+}
+
 async function minifyCSS(css: string, config: ResolvedConfig) {
   const { code, warnings } = await transform(css, {
     loader: 'css',
@@ -1133,12 +1166,19 @@ async function rebaseUrls(
   if (fileDir === rootDir) {
     return { file }
   }
-  // no url()
+
   const content = fs.readFileSync(file, 'utf-8')
-  if (!cssUrlRE.test(content)) {
+  // no url()
+  const hasUrls = cssUrlRE.test(content)
+  // no @import xxx.css
+  const hasImportCss = importCssRE.test(content)
+
+  if (!hasUrls && !hasImportCss) {
     return { file }
   }
-  const rebased = await rewriteCssUrls(content, (url) => {
+
+  let rebased
+  const rebaseFn = (url: string) => {
     if (url.startsWith('/')) return url
     // match alias, no need to rewrite
     for (const { find } of alias) {
@@ -1151,7 +1191,17 @@ async function rebaseUrls(
     const absolute = path.resolve(fileDir, url)
     const relative = path.relative(rootDir, absolute)
     return normalizePath(relative)
-  })
+  }
+
+  // fix css imports in less such as `@import "foo.css"`
+  if (hasImportCss) {
+    rebased = await rewriteImportCss(content, rebaseFn)
+  }
+
+  if (hasUrls) {
+    rebased = await rewriteCssUrls(rebased || content, rebaseFn)
+  }
+
   return {
     file,
     contents: rebased