Skip to content

Commit

Permalink
feat(css): support sass compiler api and sass-embedded package (#17754)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Jul 31, 2024
1 parent efcd830 commit 1025bb6
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 11 deletions.
4 changes: 2 additions & 2 deletions docs/config/shared-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,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 preprocessors can be found in their respective documentation:

- `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/).
- `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/).
- `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.

Expand All @@ -244,7 +244,7 @@ export default defineConfig({
},
},
scss: {
api: 'modern', // or "legacy"
api: 'modern-compiler', // or "modern", "legacy"
importers: [
// ...
],
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ That said, Vite does provide built-in support for `.scss`, `.sass`, `.less`, `.s

```bash
# .scss and .sass
npm add -D sass
npm add -D sass-embedded # or sass

# .less
npm add -D less
Expand Down
5 changes: 5 additions & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-license": "^3.5.2",
"sass": "^1.77.8",
"sass-embedded": "^1.77.8",
"sirv": "^2.0.4",
"source-map-support": "^0.5.21",
"strip-ansi": "^7.1.0",
Expand All @@ -157,6 +158,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
Expand All @@ -168,6 +170,9 @@
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
Expand Down
117 changes: 109 additions & 8 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import { createRequire } from 'node:module'
import { fileURLToPath, pathToFileURL } from 'node:url'
import glob from 'fast-glob'
import postcssrc from 'postcss-load-config'
import type {
Expand Down Expand Up @@ -1945,7 +1946,9 @@ type StylePreprocessorOptions = {
}

type SassStylePreprocessorOptions = StylePreprocessorOptions &
Omit<Sass.LegacyOptions<'async'>, 'data' | 'file' | 'outFile'>
Omit<Sass.LegacyOptions<'async'>, 'data' | 'file' | 'outFile'> & {
api?: 'legacy' | 'modern' | 'modern-compiler'
}

type StylusStylePreprocessorOptions = StylePreprocessorOptions & {
define?: Record<string, any>
Expand Down Expand Up @@ -1990,11 +1993,11 @@ export interface StylePreprocessorResults {
}

const loadedPreprocessorPath: Partial<
Record<PreprocessLang | PostCssDialectLang, string>
Record<PreprocessLang | PostCssDialectLang | 'sass-embedded', string>
> = {}

function loadPreprocessorPath(
lang: PreprocessLang | PostCssDialectLang,
lang: PreprocessLang | PostCssDialectLang | 'sass-embedded',
root: string,
): string {
const cached = loadedPreprocessorPath[lang]
Expand All @@ -2020,6 +2023,24 @@ function loadPreprocessorPath(
}
}

function loadSassPackage(root: string): {
name: 'sass' | 'sass-embedded'
path: string
} {
// try sass-embedded before sass
try {
const path = loadPreprocessorPath('sass-embedded', root)
return { name: 'sass-embedded', path }
} catch (e1) {
try {
const path = loadPreprocessorPath(PreprocessLang.sass, root)
return { name: 'sass', path }
} catch (e2) {
throw e1
}
}
}

let cachedSss: any
function loadSss(root: string) {
if (cachedSss) return cachedSss
Expand Down Expand Up @@ -2277,6 +2298,81 @@ const makeModernScssWorker = (
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 = (
resolvers: CSSAtImportResolvers,
alias: Alias[],
_maxWorkers: number | undefined,
) => {
let compiler: Sass.AsyncCompiler | undefined

const worker: Awaited<ReturnType<typeof makeModernScssWorker>> = {
async run(sassPath, data, options) {
// need pathToFileURL for windows since import("D:...") fails
// https://github.com/nodejs/node/issues/31710
const sass: typeof Sass = (await import(pathToFileURL(sassPath).href))
.default
compiler ??= await sass.initAsyncCompiler()

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 resolvers.sass(url, cleanScssBugUrl(importer))
return resolved ? pathToFileURL(resolved) : 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 result = await rebaseUrls(
fileURLToPath(canonicalUrl),
options.filename,
alias,
'$',
resolvers.sass,
)
const contents =
result.contents ?? (await fsp.readFile(result.file, 'utf-8'))
return { contents, syntax }
},
}
sassOptions.importers = [
...(sassOptions.importers ?? []),
internalImporter,
]

const result = await compiler.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
},
async stop() {
compiler?.dispose()
compiler = undefined
},
}

return worker
}

type ScssWorkerResult = {
css: string
map?: string | undefined
Expand All @@ -2295,14 +2391,19 @@ const scssProcessor = (
}
},
async process(source, root, options, resolvers) {
const sassPath = loadPreprocessorPath(PreprocessLang.sass, root)
const sassPackage = loadSassPackage(root)
// TODO: change default in v6
// options.api ?? sassPackage.name === "sass-embedded" ? "modern-compiler" : "modern";
const api = options.api ?? 'legacy'

if (!workerMap.has(options.alias)) {
workerMap.set(
options.alias,
options.api === 'modern'
? makeModernScssWorker(resolvers, options.alias, maxWorkers)
: makeScssWorker(resolvers, options.alias, maxWorkers),
api === 'modern-compiler'
? makeModernCompilerScssWorker(resolvers, options.alias, maxWorkers)
: api === 'modern'
? makeModernScssWorker(resolvers, options.alias, maxWorkers)
: makeScssWorker(resolvers, options.alias, maxWorkers),
)
}
const worker = workerMap.get(options.alias)!
Expand All @@ -2320,7 +2421,7 @@ const scssProcessor = (
}
try {
const result = await worker.run(
sassPath,
sassPackage.path,
data,
optionsWithoutAdditionalData,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '../css.spec'
31 changes: 31 additions & 0 deletions playground/css/vite.config-sass-modern-compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineConfig } from 'vite'
import baseConfig from './vite.config.js'

export default defineConfig({
...baseConfig,
css: {
...baseConfig.css,
preprocessorOptions: {
...baseConfig.css.preprocessorOptions,
scss: {
api: 'modern-compiler',
additionalData: `$injectedColor: orange;`,
importers: [
{
canonicalize(url) {
return url === 'virtual-dep'
? new URL('custom-importer:virtual-dep')
: null
},
load() {
return {
contents: ``,
syntax: 'scss',
}
},
},
],
},
},
},
})
5 changes: 5 additions & 0 deletions playground/vitestGlobalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export async function setup({ provide }: GlobalSetupContext): Promise<void> {
path.resolve(tempDir, 'css__sass-modern'),
{ recursive: true },
)
await fs.cp(
path.resolve(tempDir, 'css'),
path.resolve(tempDir, 'css__sass-modern-compiler'),
{ recursive: true },
)
}

export async function teardown(): Promise<void> {
Expand Down
Loading

0 comments on commit 1025bb6

Please sign in to comment.