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: support SASS modern api #7170

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/playground/sass-modern/css-dep/index.css
@@ -0,0 +1,3 @@
.css-dep {
color: purple;
}
1 change: 1 addition & 0 deletions packages/playground/sass-modern/css-dep/index.js
@@ -0,0 +1 @@
throw new Error('should not be imported')
3 changes: 3 additions & 0 deletions packages/playground/sass-modern/css-dep/index.scss
@@ -0,0 +1,3 @@
.css-dep-sass {
color: orange;
}
2 changes: 2 additions & 0 deletions packages/playground/sass-modern/css-dep/index.styl
@@ -0,0 +1,2 @@
.css-dep-stylus
color red
8 changes: 8 additions & 0 deletions packages/playground/sass-modern/css-dep/package.json
@@ -0,0 +1,8 @@
{
"name": "css-dep",
"private": true,
"version": "1.0.0",
"main": "index.js",
"style": "index.css",
"sass": "index.scss"
}
10 changes: 10 additions & 0 deletions packages/playground/sass-modern/index.html
@@ -0,0 +1,10 @@
<div>
<p class="sass">SASS: This should be orange</p>
<p>Imported SASS string:</p>
<pre class="imported-sass"></pre>
<p class="css-dep-sass">
@import dependency w/ sass enrtrypoints: this should be orange
</p>
</div>

<script type="module" src="./main.js"></script>
6 changes: 6 additions & 0 deletions packages/playground/sass-modern/main.js
@@ -0,0 +1,6 @@
import sass from './sass.scss'
text('.imported-sass', sass)

function text(el, text) {
document.querySelector(el).textContent = text
}
15 changes: 15 additions & 0 deletions packages/playground/sass-modern/package.json
@@ -0,0 +1,15 @@
{
"name": "test-css",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"debug": "node --inspect-brk ../../vite/bin/vite",
"preview": "vite preview"
},
"devDependencies": {
"sass": "^1.49.9",
"cross-env": "^7.0.3"
}
}
6 changes: 6 additions & 0 deletions packages/playground/sass-modern/sass.scss
@@ -0,0 +1,6 @@
@import 'css-dep'; // package w/ sass entry points

.sass {
/* injected via vite.config.js */
color: $injectedColor;
}
15 changes: 15 additions & 0 deletions packages/playground/sass-modern/vite.config.js
@@ -0,0 +1,15 @@
const path = require('path')

/**
* @type {import('vite').UserConfig}
*/
module.exports = {
css: {
preprocessorOptions: {
scss: {
api: 'modern',
additionalData: `$injectedColor: orange;`
}
}
}
}
2 changes: 1 addition & 1 deletion packages/vite/package.json
Expand Up @@ -73,7 +73,6 @@
"@types/mime": "^2.0.3",
"@types/node": "^16.11.22",
"@types/resolve": "^1.20.1",
"@types/sass": "~1.43.1",
"@types/stylus": "^0.48.36",
"@types/ws": "^8.2.2",
"@vue/compiler-dom": "^3.2.30",
Expand Down Expand Up @@ -108,6 +107,7 @@
"postcss-modules": "^4.3.0",
"resolve.exports": "^1.1.0",
"rollup-plugin-license": "^2.6.1",
"sass": "~1.49.9",
"sirv": "^2.0.2",
"source-map-support": "^0.5.21",
"strip-ansi": "^6.0.1",
Expand Down
151 changes: 107 additions & 44 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -44,6 +44,7 @@ import type { Alias } from 'types/alias'
import type { ModuleNode } from '../server/moduleGraph'
import { transform, formatMessages } from 'esbuild'
import { addToHTMLProxyTransformResult } from './html'
import { pathToFileURL, fileURLToPath } from 'url'

// const debug = createDebugger('vite:css')

Expand Down Expand Up @@ -1007,9 +1008,14 @@ type StylePreprocessorOptions = {
additionalData?: PreprocessorAdditionalData
filename: string
alias: Alias[]
api: 'legacy' | 'modern'
}

type SassStylePreprocessorOptions = StylePreprocessorOptions & Sass.Options
type SassStylePreprocessorOptions = StylePreprocessorOptions &
Sass.StringOptions<'sync'>

type SassStylePreprocessorLegacyOptions = StylePreprocessorOptions &
Sass.LegacyOptions<'sync'>

type StylePreprocessor = (
source: string,
Expand All @@ -1021,7 +1027,7 @@ type StylePreprocessor = (
type SassStylePreprocessor = (
source: string,
root: string,
options: SassStylePreprocessorOptions,
options: SassStylePreprocessorOptions | SassStylePreprocessorLegacyOptions,
resolvers: CSSAtImportResolvers
) => StylePreprocessorResults | Promise<StylePreprocessorResults>

Expand Down Expand Up @@ -1073,55 +1079,112 @@ const scss: SassStylePreprocessor = async (
options,
resolvers
) => {
const render = loadPreprocessor(PreprocessLang.sass, root).render
const internalImporter: Sass.Importer = (url, importer, done) => {
resolvers.sass(url, importer).then((resolved) => {
if (resolved) {
rebaseUrls(resolved, options.filename, options.alias)
.then((data) => done?.(data))
.catch((data) => done?.(data))
} else {
done?.(null)
const isModern = options.api === 'modern'

if (isModern) {
const compileString = loadPreprocessor(
PreprocessLang.sass,
root
).compileString

// TODO: we can't execute vite's internal resolver because we don't have prev in new importer API (as we have in old)
const internalImporter: Sass.Importer = {
canonicalize(url, options) {
return null
},
load(canonicalUrl) {
return null
}
})
}
const importer = [internalImporter]
if (options.importer) {
Array.isArray(options.importer)
? importer.push(...options.importer)
: importer.push(options.importer)
}
}
const importers = [internalImporter]
if (options.importers && Array.isArray(options.importers)) {
importers.push(...options.importers)
}

const finalOptions: Sass.Options = {
...options,
data: await getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
importer
}
if (typeof options.syntax === 'undefined') {
const ext = path.extname(options.filename)

try {
const result = await new Promise<Sass.Result>((resolve, reject) => {
render(finalOptions, (err, res) => {
if (err) {
reject(err)
if (ext && ext.toLowerCase() === '.scss') {
options.syntax = 'scss'
} else if (ext && ext.toLowerCase() === '.sass') {
options.syntax = 'indented'
} else if (ext && ext.toLowerCase() === '.css') {
options.syntax = 'css'
}
}

const finalOptions: Sass.StringOptions<'sync'> = {
...(options as Sass.StringOptions<'sync'>),
url: pathToFileURL(options.filename),
importers
}

try {
const result = compileString(
await getSource(source, options.filename, options.additionalData),
finalOptions
)
const deps = result.loadedUrls.map((url) => fileURLToPath(url))

return {
code: result.css,
errors: [],
deps
}
} catch (e) {
return { code: '', errors: [e], deps: [] }
}
} else {
const render = loadPreprocessor(PreprocessLang.sass, root).render
const internalImporter: Sass.LegacyImporter = (url, importer, done) => {
resolvers.sass(url, importer).then((resolved) => {
if (resolved) {
rebaseUrls(resolved, options.filename, options.alias)
.then((data) => done?.(data))
.catch((data) => done?.(data))
} else {
resolve(res)
done?.(null)
}
})
})
const deps = result.stats.includedFiles
}
const importer = [internalImporter]
if (options.importer) {
Array.isArray(options.importer)
? importer.push(...options.importer)
: importer.push(options.importer)
}

return {
code: result.css.toString(),
errors: [],
deps
const finalOptions: Sass.LegacyOptions<'async'> = {
...(options as Sass.LegacyOptions<'sync'>),
data: await getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
importer
}

try {
const result = await new Promise<Sass.LegacyResult>((resolve, reject) => {
render(finalOptions, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res!)
}
})
})
const deps = result.stats.includedFiles

return {
code: result.css.toString(),
errors: [],
deps
}
} catch (e) {
// normalize SASS error
e.id = e.file
e.frame = e.formatted
return { code: '', errors: [e], deps: [] }
}
} catch (e) {
// normalize SASS error
e.id = e.file
e.frame = e.formatted
return { code: '', errors: [e], deps: [] }
}
}

Expand All @@ -1144,7 +1207,7 @@ async function rebaseUrls(
file: string,
rootFile: string,
alias: Alias[]
): Promise<Sass.ImporterReturnType> {
): Promise<Sass.LegacyImporterResult> {
file = path.resolve(file) // ensure os-specific flashes
// in the same dir, no need to rebase
const fileDir = path.dirname(file)
Expand Down
40 changes: 25 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.