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) {