Skip to content

Commit 1025bb6

Browse files
authored
feat(css): support sass compiler api and sass-embedded package (#17754)
1 parent efcd830 commit 1025bb6

File tree

8 files changed

+384
-11
lines changed

8 files changed

+384
-11
lines changed

docs/config/shared-options.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ Note if an inline config is provided, Vite will not search for other PostCSS con
225225

226226
Specify options to pass to CSS pre-processors. The file extensions are used as keys for the options. The supported options for each preprocessors can be found in their respective documentation:
227227

228-
- `sass`/`scss` - top level option `api: "legacy" | "modern"` (default `"legacy"`) allows switching which sass API to use. [Options (legacy)](https://sass-lang.com/documentation/js-api/interfaces/LegacyStringOptions), [Options (modern)](https://sass-lang.com/documentation/js-api/interfaces/stringoptions/).
228+
- `sass`/`scss` - top level option `api: "legacy" | "modern" | "modern-compiler"` (default `"legacy"`) allows switching which sass API to use. For the best performance, it's recommended to use `api: "modern-compiler"` with `sass-embedded` package. [Options (legacy)](https://sass-lang.com/documentation/js-api/interfaces/LegacyStringOptions), [Options (modern)](https://sass-lang.com/documentation/js-api/interfaces/stringoptions/).
229229
- `less` - [Options](https://lesscss.org/usage/#less-options).
230230
- `styl`/`stylus` - Only [`define`](https://stylus-lang.com/docs/js.html#define-name-node) is supported, which can be passed as an object.
231231

@@ -244,7 +244,7 @@ export default defineConfig({
244244
},
245245
},
246246
scss: {
247-
api: 'modern', // or "legacy"
247+
api: 'modern-compiler', // or "modern", "legacy"
248248
importers: [
249249
// ...
250250
],

docs/guide/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ That said, Vite does provide built-in support for `.scss`, `.sass`, `.less`, `.s
257257

258258
```bash
259259
# .scss and .sass
260-
npm add -D sass
260+
npm add -D sass-embedded # or sass
261261

262262
# .less
263263
npm add -D less

packages/vite/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"rollup-plugin-esbuild": "^6.1.1",
143143
"rollup-plugin-license": "^3.5.2",
144144
"sass": "^1.77.8",
145+
"sass-embedded": "^1.77.8",
145146
"sirv": "^2.0.4",
146147
"source-map-support": "^0.5.21",
147148
"strip-ansi": "^7.1.0",
@@ -157,6 +158,7 @@
157158
"less": "*",
158159
"lightningcss": "^1.21.0",
159160
"sass": "*",
161+
"sass-embedded": "*",
160162
"stylus": "*",
161163
"sugarss": "*",
162164
"terser": "^5.4.0"
@@ -168,6 +170,9 @@
168170
"sass": {
169171
"optional": true
170172
},
173+
"sass-embedded": {
174+
"optional": true
175+
},
171176
"stylus": {
172177
"optional": true
173178
},

packages/vite/src/node/plugins/css.ts

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs'
22
import fsp from 'node:fs/promises'
33
import path from 'node:path'
44
import { createRequire } from 'node:module'
5+
import { fileURLToPath, pathToFileURL } from 'node:url'
56
import glob from 'fast-glob'
67
import postcssrc from 'postcss-load-config'
78
import type {
@@ -1945,7 +1946,9 @@ type StylePreprocessorOptions = {
19451946
}
19461947

19471948
type SassStylePreprocessorOptions = StylePreprocessorOptions &
1948-
Omit<Sass.LegacyOptions<'async'>, 'data' | 'file' | 'outFile'>
1949+
Omit<Sass.LegacyOptions<'async'>, 'data' | 'file' | 'outFile'> & {
1950+
api?: 'legacy' | 'modern' | 'modern-compiler'
1951+
}
19491952

19501953
type StylusStylePreprocessorOptions = StylePreprocessorOptions & {
19511954
define?: Record<string, any>
@@ -1990,11 +1993,11 @@ export interface StylePreprocessorResults {
19901993
}
19911994

19921995
const loadedPreprocessorPath: Partial<
1993-
Record<PreprocessLang | PostCssDialectLang, string>
1996+
Record<PreprocessLang | PostCssDialectLang | 'sass-embedded', string>
19941997
> = {}
19951998

19961999
function loadPreprocessorPath(
1997-
lang: PreprocessLang | PostCssDialectLang,
2000+
lang: PreprocessLang | PostCssDialectLang | 'sass-embedded',
19982001
root: string,
19992002
): string {
20002003
const cached = loadedPreprocessorPath[lang]
@@ -2020,6 +2023,24 @@ function loadPreprocessorPath(
20202023
}
20212024
}
20222025

2026+
function loadSassPackage(root: string): {
2027+
name: 'sass' | 'sass-embedded'
2028+
path: string
2029+
} {
2030+
// try sass-embedded before sass
2031+
try {
2032+
const path = loadPreprocessorPath('sass-embedded', root)
2033+
return { name: 'sass-embedded', path }
2034+
} catch (e1) {
2035+
try {
2036+
const path = loadPreprocessorPath(PreprocessLang.sass, root)
2037+
return { name: 'sass', path }
2038+
} catch (e2) {
2039+
throw e1
2040+
}
2041+
}
2042+
}
2043+
20232044
let cachedSss: any
20242045
function loadSss(root: string) {
20252046
if (cachedSss) return cachedSss
@@ -2277,6 +2298,81 @@ const makeModernScssWorker = (
22772298
return worker
22782299
}
22792300

2301+
// this is mostly a copy&paste of makeModernScssWorker
2302+
// however sharing code between two is hard because
2303+
// makeModernScssWorker above needs function inlined for worker.
2304+
const makeModernCompilerScssWorker = (
2305+
resolvers: CSSAtImportResolvers,
2306+
alias: Alias[],
2307+
_maxWorkers: number | undefined,
2308+
) => {
2309+
let compiler: Sass.AsyncCompiler | undefined
2310+
2311+
const worker: Awaited<ReturnType<typeof makeModernScssWorker>> = {
2312+
async run(sassPath, data, options) {
2313+
// need pathToFileURL for windows since import("D:...") fails
2314+
// https://github.com/nodejs/node/issues/31710
2315+
const sass: typeof Sass = (await import(pathToFileURL(sassPath).href))
2316+
.default
2317+
compiler ??= await sass.initAsyncCompiler()
2318+
2319+
const sassOptions = { ...options } as Sass.StringOptions<'async'>
2320+
sassOptions.url = pathToFileURL(options.filename)
2321+
sassOptions.sourceMap = options.enableSourcemap
2322+
2323+
const internalImporter: Sass.Importer<'async'> = {
2324+
async canonicalize(url, context) {
2325+
const importer = context.containingUrl
2326+
? fileURLToPath(context.containingUrl)
2327+
: options.filename
2328+
const resolved = await resolvers.sass(url, cleanScssBugUrl(importer))
2329+
return resolved ? pathToFileURL(resolved) : null
2330+
},
2331+
async load(canonicalUrl) {
2332+
const ext = path.extname(canonicalUrl.pathname)
2333+
let syntax: Sass.Syntax = 'scss'
2334+
if (ext === '.sass') {
2335+
syntax = 'indented'
2336+
} else if (ext === '.css') {
2337+
syntax = 'css'
2338+
}
2339+
const result = await rebaseUrls(
2340+
fileURLToPath(canonicalUrl),
2341+
options.filename,
2342+
alias,
2343+
'$',
2344+
resolvers.sass,
2345+
)
2346+
const contents =
2347+
result.contents ?? (await fsp.readFile(result.file, 'utf-8'))
2348+
return { contents, syntax }
2349+
},
2350+
}
2351+
sassOptions.importers = [
2352+
...(sassOptions.importers ?? []),
2353+
internalImporter,
2354+
]
2355+
2356+
const result = await compiler.compileStringAsync(data, sassOptions)
2357+
return {
2358+
css: result.css,
2359+
map: result.sourceMap ? JSON.stringify(result.sourceMap) : undefined,
2360+
stats: {
2361+
includedFiles: result.loadedUrls
2362+
.filter((url) => url.protocol === 'file:')
2363+
.map((url) => fileURLToPath(url)),
2364+
},
2365+
} satisfies ScssWorkerResult
2366+
},
2367+
async stop() {
2368+
compiler?.dispose()
2369+
compiler = undefined
2370+
},
2371+
}
2372+
2373+
return worker
2374+
}
2375+
22802376
type ScssWorkerResult = {
22812377
css: string
22822378
map?: string | undefined
@@ -2295,14 +2391,19 @@ const scssProcessor = (
22952391
}
22962392
},
22972393
async process(source, root, options, resolvers) {
2298-
const sassPath = loadPreprocessorPath(PreprocessLang.sass, root)
2394+
const sassPackage = loadSassPackage(root)
2395+
// TODO: change default in v6
2396+
// options.api ?? sassPackage.name === "sass-embedded" ? "modern-compiler" : "modern";
2397+
const api = options.api ?? 'legacy'
22992398

23002399
if (!workerMap.has(options.alias)) {
23012400
workerMap.set(
23022401
options.alias,
2303-
options.api === 'modern'
2304-
? makeModernScssWorker(resolvers, options.alias, maxWorkers)
2305-
: makeScssWorker(resolvers, options.alias, maxWorkers),
2402+
api === 'modern-compiler'
2403+
? makeModernCompilerScssWorker(resolvers, options.alias, maxWorkers)
2404+
: api === 'modern'
2405+
? makeModernScssWorker(resolvers, options.alias, maxWorkers)
2406+
: makeScssWorker(resolvers, options.alias, maxWorkers),
23062407
)
23072408
}
23082409
const worker = workerMap.get(options.alias)!
@@ -2320,7 +2421,7 @@ const scssProcessor = (
23202421
}
23212422
try {
23222423
const result = await worker.run(
2323-
sassPath,
2424+
sassPackage.path,
23242425
data,
23252426
optionsWithoutAdditionalData,
23262427
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '../css.spec'
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { defineConfig } from 'vite'
2+
import baseConfig from './vite.config.js'
3+
4+
export default defineConfig({
5+
...baseConfig,
6+
css: {
7+
...baseConfig.css,
8+
preprocessorOptions: {
9+
...baseConfig.css.preprocessorOptions,
10+
scss: {
11+
api: 'modern-compiler',
12+
additionalData: `$injectedColor: orange;`,
13+
importers: [
14+
{
15+
canonicalize(url) {
16+
return url === 'virtual-dep'
17+
? new URL('custom-importer:virtual-dep')
18+
: null
19+
},
20+
load() {
21+
return {
22+
contents: ``,
23+
syntax: 'scss',
24+
}
25+
},
26+
},
27+
],
28+
},
29+
},
30+
},
31+
})

playground/vitestGlobalSetup.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export async function setup({ provide }: GlobalSetupContext): Promise<void> {
4747
path.resolve(tempDir, 'css__sass-modern'),
4848
{ recursive: true },
4949
)
50+
await fs.cp(
51+
path.resolve(tempDir, 'css'),
52+
path.resolve(tempDir, 'css__sass-modern-compiler'),
53+
{ recursive: true },
54+
)
5055
}
5156

5257
export async function teardown(): Promise<void> {

0 commit comments

Comments
 (0)