mirror of
https://github.com/vitejs/vite.git
synced 2024-11-22 07:09:05 +00:00
feat(css): support sass modern api (#17728)
This commit is contained in:
parent
116e37acf1
commit
73a3de01d2
@ -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` - [Options](https://sass-lang.com/documentation/js-api/interfaces/LegacyStringOptions).
|
||||
- `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/).
|
||||
- `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.
|
||||
|
||||
@ -243,6 +243,12 @@ export default defineConfig({
|
||||
$specialColor: new stylus.nodes.RGBA(51, 197, 255, 1),
|
||||
},
|
||||
},
|
||||
scss: {
|
||||
api: 'modern', // or "legacy"
|
||||
importers: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -2110,7 +2110,7 @@ const makeScssWorker = (
|
||||
// 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 = require('node:path')
|
||||
const path: typeof import('node:path') = require('node:path')
|
||||
|
||||
// NOTE: `sass` always runs it's own importer first, and only falls back to
|
||||
// the `importer` option when it can't resolve a path
|
||||
@ -2144,11 +2144,7 @@ const makeScssWorker = (
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
return new Promise<{
|
||||
css: string
|
||||
map?: string | undefined
|
||||
stats: Sass.LegacyResult['stats']
|
||||
}>((resolve, reject) => {
|
||||
return new Promise<ScssWorkerResult>((resolve, reject) => {
|
||||
sass.render(finalOptions, (err, res) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
@ -2179,6 +2175,114 @@ const makeScssWorker = (
|
||||
return worker
|
||||
}
|
||||
|
||||
const makeModernScssWorker = (
|
||||
resolvers: CSSAtImportResolvers,
|
||||
alias: Alias[],
|
||||
maxWorkers: number | undefined,
|
||||
) => {
|
||||
const internalCanonicalize = async (
|
||||
url: string,
|
||||
importer: string,
|
||||
): Promise<string | null> => {
|
||||
importer = cleanScssBugUrl(importer)
|
||||
const resolved = await resolvers.sass(url, importer)
|
||||
return resolved ?? null
|
||||
}
|
||||
|
||||
const internalLoad = async (file: string, rootFile: string) => {
|
||||
const result = await rebaseUrls(file, rootFile, alias, '$', resolvers.sass)
|
||||
if (result.contents) {
|
||||
return result.contents
|
||||
}
|
||||
return await fsp.readFile(result.file, 'utf-8')
|
||||
}
|
||||
|
||||
const worker = new WorkerWithFallback(
|
||||
() =>
|
||||
async (
|
||||
sassPath: string,
|
||||
data: string,
|
||||
// additionalData can a function that is not cloneable but it won't be used
|
||||
options: SassStylePreprocessorOptions & { 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)
|
||||
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 contents = await internalLoad(
|
||||
fileURLToPath(canonicalUrl),
|
||||
options.filename,
|
||||
)
|
||||
return { contents, syntax }
|
||||
},
|
||||
}
|
||||
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))
|
||||
)
|
||||
},
|
||||
max: maxWorkers,
|
||||
},
|
||||
)
|
||||
return worker
|
||||
}
|
||||
|
||||
type ScssWorkerResult = {
|
||||
css: string
|
||||
map?: string | undefined
|
||||
stats: Pick<Sass.LegacyResult['stats'], 'includedFiles'>
|
||||
}
|
||||
|
||||
const scssProcessor = (
|
||||
maxWorkers: number | undefined,
|
||||
): SassStylePreprocessor => {
|
||||
@ -2196,7 +2300,9 @@ const scssProcessor = (
|
||||
if (!workerMap.has(options.alias)) {
|
||||
workerMap.set(
|
||||
options.alias,
|
||||
makeScssWorker(resolvers, options.alias, maxWorkers),
|
||||
options.api === 'modern'
|
||||
? makeModernScssWorker(resolvers, options.alias, maxWorkers)
|
||||
: makeScssWorker(resolvers, options.alias, maxWorkers),
|
||||
)
|
||||
}
|
||||
const worker = workerMap.get(options.alias)!
|
||||
@ -2251,7 +2357,7 @@ async function rebaseUrls(
|
||||
alias: Alias[],
|
||||
variablePrefix: string,
|
||||
resolver: ResolveFn,
|
||||
): Promise<Sass.LegacyImporterResult> {
|
||||
): Promise<{ file: string; contents?: string }> {
|
||||
file = path.resolve(file) // ensure os-specific flashes
|
||||
// in the same dir, no need to rebase
|
||||
const fileDir = path.dirname(file)
|
||||
@ -2681,7 +2787,7 @@ const createPreprocessorWorkerController = (maxWorkers: number | undefined) => {
|
||||
return scss.process(
|
||||
source,
|
||||
root,
|
||||
{ ...options, indentedSyntax: true },
|
||||
{ ...options, indentedSyntax: true, syntax: 'indented' },
|
||||
resolvers,
|
||||
)
|
||||
}
|
||||
|
1
playground/css/__tests__/sass-modern/sass-modern.spec.ts
Normal file
1
playground/css/__tests__/sass-modern/sass-modern.spec.ts
Normal file
@ -0,0 +1 @@
|
||||
import '../css.spec'
|
31
playground/css/vite.config-sass-modern.js
Normal file
31
playground/css/vite.config-sass-modern.js
Normal file
@ -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',
|
||||
additionalData: `$injectedColor: orange;`,
|
||||
importers: [
|
||||
{
|
||||
canonicalize(url) {
|
||||
return url === 'virtual-dep'
|
||||
? new URL('custom-importer:virtual-dep')
|
||||
: null
|
||||
},
|
||||
load() {
|
||||
return {
|
||||
contents: ``,
|
||||
syntax: 'scss',
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
@ -41,6 +41,12 @@ export async function setup({ provide }: GlobalSetupContext): Promise<void> {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
// also setup dedicated copy for "variant" tests
|
||||
await fs.cp(
|
||||
path.resolve(tempDir, 'css'),
|
||||
path.resolve(tempDir, 'css__sass-modern'),
|
||||
{ recursive: true },
|
||||
)
|
||||
}
|
||||
|
||||
export async function teardown(): Promise<void> {
|
||||
|
@ -136,6 +136,15 @@ beforeAll(async (s) => {
|
||||
const testCustomRoot = path.resolve(testDir, 'root')
|
||||
rootDir = fs.existsSync(testCustomRoot) ? testCustomRoot : testDir
|
||||
|
||||
// separate rootDir for variant
|
||||
const variantName = path.basename(path.dirname(testPath))
|
||||
if (variantName !== '__tests__') {
|
||||
const variantTestDir = testDir + '__' + variantName
|
||||
if (fs.existsSync(variantTestDir)) {
|
||||
rootDir = testDir = variantTestDir
|
||||
}
|
||||
}
|
||||
|
||||
const testCustomServe = [
|
||||
path.resolve(path.dirname(testPath), 'serve.ts'),
|
||||
path.resolve(path.dirname(testPath), 'serve.js'),
|
||||
|
Loading…
Reference in New Issue
Block a user