Skip to content

Commit

Permalink
feat: support modern SASS api
Browse files Browse the repository at this point in the history
  • Loading branch information
ygj6 committed Mar 4, 2022
1 parent 87c9e3d commit 5725b5e
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 60 deletions.
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"
}
8 changes: 8 additions & 0 deletions packages/playground/sass-modern/index.html
@@ -0,0 +1,8 @@
<div>
<p class="sass">SASS: This should be orange</p>
<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 @@ -72,7 +72,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-js": "^1.0.2",
"source-map-support": "^0.5.21",
Expand Down
150 changes: 106 additions & 44 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -988,9 +988,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 @@ -1002,7 +1007,7 @@ type StylePreprocessor = (
type SassStylePreprocessor = (
source: string,
root: string,
options: SassStylePreprocessorOptions,
options: SassStylePreprocessorOptions | SassStylePreprocessorLegacyOptions,
resolvers: CSSAtImportResolvers
) => StylePreprocessorResults | Promise<StylePreprocessorResults>

Expand Down Expand Up @@ -1054,55 +1059,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: new URL(options.filename),
importers
}

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

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 @@ -1125,7 +1187,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.

0 comments on commit 5725b5e

Please sign in to comment.