diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md
index 3e29357563abbf..b2229f3a959cd9 100644
--- a/docs/config/shared-options.md
+++ b/docs/config/shared-options.md
@@ -227,7 +227,7 @@ Note if an inline config is provided, Vite will not search for other PostCSS con
Specify options to pass to CSS pre-processors. The file extensions are used as keys for the options. The supported options for each preprocessor can be found in their respective documentation:
- `sass`/`scss`:
- - Select the sass API to use with `api: "modern-compiler" | "modern"` (default `"modern-compiler"` if `sass-embedded` is installed, otherwise `"modern"`). For the best performance, it's recommended to use `api: "modern-compiler"` with the `sass-embedded` package.
+ - Uses `sass-embedded` if installed, otherwise uses `sass`. For the best performance, it's recommended to install the `sass-embedded` package.
- [Options](https://sass-lang.com/documentation/js-api/interfaces/stringoptions/)
- `less`: [Options](https://lesscss.org/usage/#less-options).
- `styl`/`stylus`: Only [`define`](https://stylus-lang.com/docs/js.html#define-name-node) is supported, which can be passed as an object.
@@ -247,7 +247,6 @@ export default defineConfig({
},
},
scss: {
- api: 'modern-compiler', // or "modern"
importers: [
// ...
],
diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts
index 5ba7c8b055e457..c05a89d89b1329 100644
--- a/packages/vite/src/node/plugins/css.ts
+++ b/packages/vite/src/node/plugins/css.ts
@@ -2268,7 +2268,7 @@ type PreprocessorAdditionalData =
export type SassPreprocessorOptions = {
additionalData?: PreprocessorAdditionalData
-} & ({ api?: 'modern' | 'modern-compiler' } & SassModernPreprocessBaseOptions)
+} & SassModernPreprocessBaseOptions
export type LessPreprocessorOptions = {
additionalData?: PreprocessorAdditionalData
@@ -2392,156 +2392,31 @@ function cleanScssBugUrl(url: string) {
// #region Sass
// .scss/.sass processor
-const makeModernScssWorker = (
+const makeScssWorker = (
environment: PartialEnvironment,
resolvers: CSSAtImportResolvers,
alias: Alias[],
- maxWorkers: number | undefined,
+ _maxWorkers: number | undefined,
) => {
- const internalCanonicalize = async (
- url: string,
- importer: string,
- ): Promise => {
- importer = cleanScssBugUrl(importer)
- const resolved = await resolvers.sass(environment, url, importer)
- return resolved ?? null
- }
-
- const skipRebaseUrls = (unquotedUrl: string, rawUrl: string) => {
- const isQuoted = rawUrl[0] === '"' || rawUrl[0] === "'"
- // matches `url($foo)`
- if (!isQuoted && unquotedUrl[0] === '$') {
- return true
- }
- // matches `url(#{foo})` and `url('#{foo}')`
- return unquotedUrl.startsWith('#{')
- }
-
- const internalLoad = async (file: string, rootFile: string) => {
- const result = await rebaseUrls(
- environment,
- file,
- rootFile,
- alias,
- resolvers.sass,
- skipRebaseUrls,
- )
- if (result.contents) {
- return result.contents
- }
- return await fsp.readFile(result.file, 'utf-8')
- }
+ let compilerPromise: Promise | undefined
- const worker = new WorkerWithFallback(
- () =>
- async (
+ // we use the compiler api provided by sass
+ // instead of creating a worker pool on our own
+ type WorkerType = InstanceType<
+ typeof WorkerWithFallback<
+ [
sassPath: string,
data: string,
// additionalData can a function that is not cloneable but it won't be used
options: SassStylePreprocessorInternalOptions & {
- api: 'modern'
additionalData: undefined
},
- ) => {
- // eslint-disable-next-line no-restricted-globals -- this function runs inside a cjs worker
- const sass: typeof Sass = require(sassPath)
- // eslint-disable-next-line no-restricted-globals
- const path: typeof import('node:path') = require('node:path')
-
- const { fileURLToPath, pathToFileURL }: typeof import('node:url') =
- // eslint-disable-next-line no-restricted-globals
- require('node:url')
-
- const sassOptions = { ...options } as Sass.StringOptions<'async'>
- sassOptions.url = pathToFileURL(options.filename)
- sassOptions.sourceMap = options.enableSourcemap
-
- const internalImporter: Sass.Importer<'async'> = {
- async canonicalize(url, context) {
- const importer = context.containingUrl
- ? fileURLToPath(context.containingUrl)
- : options.filename
- const resolved = await internalCanonicalize(url, importer)
- if (
- resolved &&
- // only limit to these extensions because:
- // - for the `@import`/`@use`s written in file loaded by `load` function,
- // the `canonicalize` function of that `importer` is called first
- // - the `load` function of an importer is only called for the importer
- // that returned a non-null result from its `canonicalize` function
- (resolved.endsWith('.css') ||
- resolved.endsWith('.scss') ||
- resolved.endsWith('.sass'))
- ) {
- return pathToFileURL(resolved)
- }
- return null
- },
- async load(canonicalUrl) {
- const ext = path.extname(canonicalUrl.pathname)
- let syntax: Sass.Syntax = 'scss'
- if (ext === '.sass') {
- syntax = 'indented'
- } else if (ext === '.css') {
- syntax = 'css'
- }
- const contents = await internalLoad(
- fileURLToPath(canonicalUrl),
- options.filename,
- )
- return { contents, syntax, sourceMapUrl: canonicalUrl }
- },
- }
- sassOptions.importers = [
- ...(sassOptions.importers ?? []),
- internalImporter,
- ]
-
- const result = await sass.compileStringAsync(data, sassOptions)
- return {
- css: result.css,
- map: result.sourceMap ? JSON.stringify(result.sourceMap) : undefined,
- stats: {
- includedFiles: result.loadedUrls
- .filter((url) => url.protocol === 'file:')
- .map((url) => fileURLToPath(url)),
- },
- } satisfies ScssWorkerResult
- },
- {
- parentFunctions: {
- internalCanonicalize,
- internalLoad,
- },
- shouldUseFake(_sassPath, _data, options) {
- // functions and importer is a function and is not serializable
- // in that case, fallback to running in main thread
- return !!(
- (options.functions && Object.keys(options.functions).length > 0) ||
- (options.importers &&
- (!Array.isArray(options.importers) ||
- options.importers.length > 0)) ||
- options.logger
- )
- },
- max: maxWorkers,
- },
- )
- return worker
-}
-
-// this is mostly a copy&paste of makeModernScssWorker
-// however sharing code between two is hard because
-// makeModernScssWorker above needs function inlined for worker.
-const makeModernCompilerScssWorker = (
- environment: PartialEnvironment,
- resolvers: CSSAtImportResolvers,
- alias: Alias[],
- _maxWorkers: number | undefined,
-) => {
- let compilerPromise: Promise | undefined
+ ],
+ ScssWorkerResult
+ >
+ >
- const worker: Awaited> = {
+ const worker: WorkerType = {
async run(sassPath, data, options) {
// need pathToFileURL for windows since import("D:...") fails
// https://github.com/nodejs/node/issues/31710
@@ -2609,6 +2484,7 @@ const makeModernCompilerScssWorker = (
...(sassOptions.importers ?? []),
internalImporter,
]
+ sassOptions.importer ??= internalImporter
const result = await compiler.compileStringAsync(data, sassOptions)
return {
@@ -2639,12 +2515,7 @@ type ScssWorkerResult = {
const scssProcessor = (
maxWorkers: number | undefined,
): StylePreprocessor => {
- const workerMap = new Map<
- unknown,
- ReturnType<
- typeof makeModernScssWorker | typeof makeModernCompilerScssWorker
- >
- >()
+ const workerMap = new Map>()
return {
close() {
@@ -2654,26 +2525,11 @@ const scssProcessor = (
},
async process(environment, source, root, options, resolvers) {
const sassPackage = loadSassPackage(root)
- const api =
- options.api ??
- (sassPackage.name === 'sass-embedded' ? 'modern-compiler' : 'modern')
if (!workerMap.has(options.alias)) {
workerMap.set(
options.alias,
- api === 'modern-compiler'
- ? makeModernCompilerScssWorker(
- environment,
- resolvers,
- options.alias,
- maxWorkers,
- )
- : makeModernScssWorker(
- environment,
- resolvers,
- options.alias,
- maxWorkers,
- ),
+ makeScssWorker(environment, resolvers, options.alias, maxWorkers),
)
}
const worker = workerMap.get(options.alias)!
@@ -2693,7 +2549,6 @@ const scssProcessor = (
const result = await worker.run(
sassPackage.path,
data,
- // @ts-expect-error the correct worker is selected for `options.type`
optionsWithoutAdditionalData,
)
const deps = result.stats.includedFiles.map((f) => cleanScssBugUrl(f))
diff --git a/playground/css-sourcemap/__tests__/sass-modern/sass-modern.spec.ts b/playground/css-sourcemap/__tests__/sass-modern/sass-modern.spec.ts
deleted file mode 100644
index 69a693758c71cc..00000000000000
--- a/playground/css-sourcemap/__tests__/sass-modern/sass-modern.spec.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// NOTE: a separate directory from `playground/css-sourcemap` is created by playground/vitestGlobalSetup.ts
-import '../css-sourcemap.spec'
diff --git a/playground/css-sourcemap/vite.config-sass-modern.js b/playground/css-sourcemap/vite.config-sass-modern.js
deleted file mode 100644
index 739507a13caf27..00000000000000
--- a/playground/css-sourcemap/vite.config-sass-modern.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defineConfig, mergeConfig } from 'vite'
-import baseConfig from './vite.config.js'
-
-export default mergeConfig(
- baseConfig,
- defineConfig({
- css: {
- preprocessorOptions: {
- sass: {
- api: 'modern',
- },
- },
- },
- }),
-)
diff --git a/playground/css/__tests__/sass-modern/sass-modern.spec.ts b/playground/css/__tests__/sass-modern/sass-modern.spec.ts
deleted file mode 100644
index b2e4ca42c5118d..00000000000000
--- a/playground/css/__tests__/sass-modern/sass-modern.spec.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-// NOTE: a separate directory from `playground/css` is created by playground/vitestGlobalSetup.ts
-import { sassModuleTests, sassOtherTests, sassTest } from '../sass-tests'
-
-sassTest()
-sassModuleTests()
-sassOtherTests()
diff --git a/playground/css/__tests__/sass-tests.ts b/playground/css/__tests__/sass-tests.ts
index 1c0dcf7b90b17f..0f9d2c36af142d 100644
--- a/playground/css/__tests__/sass-tests.ts
+++ b/playground/css/__tests__/sass-tests.ts
@@ -14,6 +14,7 @@ export const sassTest = () => {
const imported = await page.$('.sass')
const atImport = await page.$('.sass-at-import')
const atImportAlias = await page.$('.sass-at-import-alias')
+ const atImportRelative = await page.$('.sass-at-import-relative')
const urlStartsWithVariable = await page.$('.sass-url-starts-with-variable')
const urlStartsWithVariableInterpolation1 = await page.$(
'.sass-url-starts-with-interpolation1',
@@ -38,6 +39,10 @@ export const sassTest = () => {
expect(await getBg(atImportAlias)).toMatch(
isBuild ? /base64/ : '/nested/icon.png',
)
+ expect(await getColor(atImportRelative)).toBe('olive')
+ expect(await getBg(atImportRelative)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
expect(await getBg(urlStartsWithVariable)).toMatch(
isBuild ? /ok-[-\w]+\.png/ : `${viteTestUrl}/ok.png`,
)
diff --git a/playground/css/index.html b/playground/css/index.html
index cc30e89693d920..d52e88ab5860a1 100644
--- a/playground/css/index.html
+++ b/playground/css/index.html
@@ -33,6 +33,9 @@ CSS
@import from SASS _index: This should be olive and have bg image which url
contains alias
+
+ @import from SASS relative: This should be olive and have bg image
+
@import from SASS _partial: This should be orchid
url starts with variable
diff --git a/playground/css/nested/relative.scss b/playground/css/nested/relative.scss
new file mode 100644
index 00000000000000..310c7f30433d3d
--- /dev/null
+++ b/playground/css/nested/relative.scss
@@ -0,0 +1,4 @@
+.sass-at-import-relative {
+ color: olive;
+ background: url(./icon.png) 10px no-repeat;
+}
diff --git a/playground/css/sass.scss b/playground/css/sass.scss
index 8d4bc5492e6299..fe1138c0214f14 100644
--- a/playground/css/sass.scss
+++ b/playground/css/sass.scss
@@ -1,5 +1,6 @@
@use '=/nested'; // alias + custom index resolving -> /nested/_index.scss
@use '=/nested/partial'; // sass convention: omitting leading _ for partials
+@use './nested/relative'; // relative path
@use '@vitejs/test-css-dep'; // package w/ sass entry points
@use '@vitejs/test-css-dep-exports'; // package with a sass export mapping
@use '@vitejs/test-scss-proxy-dep'; // package with a sass proxy import
diff --git a/playground/css/vite.config-sass-modern-compiler-build.js b/playground/css/vite.config-sass-modern-compiler-build.js
index b44ef1e354d4d9..412be2815eb339 100644
--- a/playground/css/vite.config-sass-modern-compiler-build.js
+++ b/playground/css/vite.config-sass-modern-compiler-build.js
@@ -17,11 +17,4 @@ export default defineConfig({
},
},
},
- css: {
- preprocessorOptions: {
- scss: {
- api: 'modern-compiler',
- },
- },
- },
})
diff --git a/playground/css/vite.config-sass-modern.js b/playground/css/vite.config-sass-modern.js
deleted file mode 100644
index 90855ac270b7d8..00000000000000
--- a/playground/css/vite.config-sass-modern.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { defineConfig } from 'vite'
-import baseConfig from './vite.config.js'
-
-export default defineConfig({
- ...baseConfig,
- css: {
- ...baseConfig.css,
- preprocessorOptions: {
- ...baseConfig.css.preprocessorOptions,
- scss: {
- .../** @type {import('vite').SassPreprocessorOptions & { api?: undefined }} */ (
- baseConfig.css.preprocessorOptions.scss
- ),
- api: 'modern',
- },
- },
- },
-})
diff --git a/playground/vitestGlobalSetup.ts b/playground/vitestGlobalSetup.ts
index a8e037d79b62e6..e8554d7e0a3a8b 100644
--- a/playground/vitestGlobalSetup.ts
+++ b/playground/vitestGlobalSetup.ts
@@ -43,8 +43,7 @@ export async function setup({ provide }: TestProject): Promise {
})
// also setup dedicated copy for "variant" tests
for (const [original, variants] of [
- ['css', ['sass-modern', 'lightningcss']],
- ['css-sourcemap', ['sass-modern']],
+ ['css', ['lightningcss']],
['transform-plugin', ['base']],
] as const) {
for (const variant of variants) {