Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(css): support sass modern api #17728

Merged
merged 30 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4948f86
feat: support sass modern api
hi-ogawa Jul 20, 2024
0fea9c4
test: add playground
hi-ogawa Jul 20, 2024
2feb149
wip: sassInternalImporter
hi-ogawa Jul 20, 2024
e572b9f
test: add custom importer example
hi-ogawa Jul 20, 2024
6619ade
test: test internal importer
hi-ogawa Jul 20, 2024
fba1d4d
chore: cleanup playground
hi-ogawa Jul 20, 2024
cbfe70d
wip: rebaseUrls
hi-ogawa Jul 20, 2024
b515950
test: test "modern" in playground/css
hi-ogawa Jul 21, 2024
f04f067
chore: cleanup
hi-ogawa Jul 21, 2024
f8154b1
debug
hi-ogawa Jul 21, 2024
1d5ce19
fix: fix stats.includedFiles
hi-ogawa Jul 21, 2024
af7e6dc
chore: cleanup
hi-ogawa Jul 21, 2024
659cd97
chore: remove playground/css-sass-modern
hi-ogawa Jul 21, 2024
8484025
chore: lockfile
hi-ogawa Jul 21, 2024
073392b
docs: update css.preprocessorOptions
hi-ogawa Jul 21, 2024
b0dac17
chore: comment
hi-ogawa Jul 21, 2024
61bc170
chore: tweak
hi-ogawa Jul 21, 2024
1ff2110
refactor: move to makeModernScssWorker
hi-ogawa Jul 23, 2024
e930f1d
chore: USE_LEGACY_SCSS flag in playground/css
hi-ogawa Jul 23, 2024
098f181
chore: cleanup old code
hi-ogawa Jul 23, 2024
96a7336
ci: test sass legacy
hi-ogawa Jul 23, 2024
c2a6256
ci: filter more
hi-ogawa Jul 23, 2024
35bf91a
test: split vite config
hi-ogawa Jul 23, 2024
a9a7038
chore: no console
hi-ogawa Jul 23, 2024
ddd3c14
test: separate `testDir` for variant tests
hi-ogawa Jul 23, 2024
6adda0e
chore: cleanup
hi-ogawa Jul 23, 2024
6e847c0
ci: cleanup
hi-ogawa Jul 23, 2024
3bf2dc1
refactor: fsp + remove toLowerCase
hi-ogawa Jul 24, 2024
2125ef6
Merge branch 'main' into feat-sass-modern
hi-ogawa Jul 29, 2024
c1df7b6
fix: fix fs, path
hi-ogawa Jul 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion 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` - [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.

Expand All @@ -243,6 +243,12 @@ export default defineConfig({
$specialColor: new stylus.nodes.RGBA(51, 197, 255, 1),
},
},
scss: {
api: 'modern', // or "legacy"
importers: [
// ...
],
},
},
},
})
Expand Down
124 changes: 115 additions & 9 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 => {
Expand All @@ -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)!
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -2681,7 +2787,7 @@ const createPreprocessorWorkerController = (maxWorkers: number | undefined) => {
return scss.process(
source,
root,
{ ...options, indentedSyntax: true },
{ ...options, indentedSyntax: true, syntax: 'indented' },
resolvers,
)
}
Expand Down
1 change: 1 addition & 0 deletions playground/css/__tests__/sass-modern/sass-modern.spec.ts
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.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',
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 @@ -40,6 +40,11 @@ export async function setup({ provide }: GlobalSetupContext): Promise<void> {
throw error
}
})
// also setup dedicated copy for "variant" tests
await fs.copy(
path.resolve(tempDir, 'css'),
path.resolve(tempDir, 'css__sass-modern'),
)
}

export async function teardown(): Promise<void> {
Expand Down
9 changes: 9 additions & 0 deletions playground/vitestSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ beforeAll(async (s) => {
const testCustomRoot = resolve(testDir, 'root')
rootDir = fs.existsSync(testCustomRoot) ? testCustomRoot : testDir

// separate rootDir for variant
const variantName = path.basename(dirname(testPath))
if (variantName !== '__tests__') {
const variantTestDir = testDir + '__' + variantName
if (fs.existsSync(variantTestDir)) {
rootDir = testDir = variantTestDir
}
}

const testCustomServe = [
resolve(dirname(testPath), 'serve.ts'),
resolve(dirname(testPath), 'serve.js'),
Expand Down