From f266bb7417167e752bbbbd64fe3c82dceb13794b Mon Sep 17 00:00:00 2001
From: Evan You
Date: Tue, 26 Jan 2021 17:56:12 -0500
Subject: [PATCH] feat: import resolving + url rebasing for less
---
.eslintrc.js | 2 +-
packages/playground/css/__tests__/css.spec.ts | 17 +
packages/playground/css/index.html | 9 +-
packages/playground/css/less.less | 7 +
packages/playground/css/main.js | 3 +
packages/playground/css/nested/index.less | 4 +
packages/playground/css/package.json | 4 +-
packages/vite/package.json | 2 +
packages/vite/src/node/plugins/css.ts | 351 ++++++++++--------
yarn.lock | 69 +++-
10 files changed, 319 insertions(+), 149 deletions(-)
create mode 100644 packages/playground/css/less.less
create mode 100644 packages/playground/css/nested/index.less
diff --git a/.eslintrc.js b/.eslintrc.js
index fd32b4579e40f8..64ccc552427a80 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -38,7 +38,7 @@ module.exports = {
'node/no-extraneous-import': [
'error',
{
- allowModules: ['vite']
+ allowModules: ['vite', 'less', 'sass']
}
],
'node/no-extraneous-require': [
diff --git a/packages/playground/css/__tests__/css.spec.ts b/packages/playground/css/__tests__/css.spec.ts
index f4b547761286f1..51ee9bd3a58ce6 100644
--- a/packages/playground/css/__tests__/css.spec.ts
+++ b/packages/playground/css/__tests__/css.spec.ts
@@ -72,6 +72,23 @@ test('sass', async () => {
await untilUpdated(() => getColor(atImport), 'blue')
})
+test('less', async () => {
+ const imported = await page.$('.less')
+ const atImport = await page.$('.less-at-import')
+
+ expect(await getColor(imported)).toBe('blue')
+ expect(await getColor(atImport)).toBe('darkslateblue')
+ expect(await getBg(atImport)).toMatch(isBuild ? /base64/ : '/nested/icon.png')
+
+ editFile('less.less', (code) => code.replace('@color: blue', '@color: red'))
+ await untilUpdated(() => getColor(imported), 'red')
+
+ editFile('nested/index.less', (code) =>
+ code.replace('color: darkslateblue', 'color: blue')
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+})
+
test('css modules', async () => {
const imported = await page.$('.modules')
expect(await getColor(imported)).toBe('turquoise')
diff --git a/packages/playground/css/index.html b/packages/playground/css/index.html
index 3f17e358098ec0..2333f5cb4d4313 100644
--- a/packages/playground/css/index.html
+++ b/packages/playground/css/index.html
@@ -17,13 +17,20 @@ CSS
PostCSS nesting plugin: this should be pink
- sass: This should be orange
+ SASS: This should be orange
@import from SASS: This should be olive and have bg image
Imported SASS string:
+ Less: This should be blue
+
+ @import from Less: This should be darkslateblue and have bg image
+
+ Imported Less string:
+
+
CSS modules: this should be turquoise
Imported CSS module:
diff --git a/packages/playground/css/less.less b/packages/playground/css/less.less
new file mode 100644
index 00000000000000..59dd3da659c6c8
--- /dev/null
+++ b/packages/playground/css/less.less
@@ -0,0 +1,7 @@
+@import '@/nested'; // alias + index resolving -> /nested/index.less
+
+@color: blue;
+
+.less {
+ color: @color;
+}
diff --git a/packages/playground/css/main.js b/packages/playground/css/main.js
index d019804190661c..b3d256a60544a5 100644
--- a/packages/playground/css/main.js
+++ b/packages/playground/css/main.js
@@ -4,6 +4,9 @@ text('.imported-css', css)
import sass from './sass.scss'
text('.imported-sass', sass)
+import less from './less.less'
+text('.imported-less', less)
+
import mod from './mod.module.css'
document.querySelector('.modules').classList.add(mod.applyColor)
text('.modules-code', JSON.stringify(mod, null, 2))
diff --git a/packages/playground/css/nested/index.less b/packages/playground/css/nested/index.less
new file mode 100644
index 00000000000000..f6d00613c9e4a1
--- /dev/null
+++ b/packages/playground/css/nested/index.less
@@ -0,0 +1,4 @@
+.less-at-import {
+ color: darkslateblue;
+ background: url(./icon.png) 10px no-repeat;
+}
diff --git a/packages/playground/css/package.json b/packages/playground/css/package.json
index 7822dfca4ea45a..a5e0140e6da380 100644
--- a/packages/playground/css/package.json
+++ b/packages/playground/css/package.json
@@ -9,6 +9,8 @@
"serve": "vite preview"
},
"devDependencies": {
- "postcss-nested": "^5.0.3"
+ "less": "^4.1.0",
+ "postcss-nested": "^5.0.3",
+ "sass": "^1.32.5"
}
}
diff --git a/packages/vite/package.json b/packages/vite/package.json
index 8971ad2166bfae..eb11b51605dad3 100644
--- a/packages/vite/package.json
+++ b/packages/vite/package.json
@@ -66,9 +66,11 @@
"@types/es-module-lexer": "^0.3.0",
"@types/estree": "^0.0.45",
"@types/etag": "^1.8.0",
+ "@types/less": "^3.0.2",
"@types/mime": "^2.0.3",
"@types/node": "^14.14.10",
"@types/resolve": "^1.17.1",
+ "@types/sass": "^1.16.0",
"@types/ws": "^7.4.0",
"@vue/compiler-dom": "^3.0.4",
"acorn": "^8.0.4",
diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts
index 605e6bfe5e2aad..6adac9ef986ce9 100644
--- a/packages/vite/src/node/plugins/css.ts
+++ b/packages/vite/src/node/plugins/css.ts
@@ -32,6 +32,13 @@ import {
import { ResolveFn, ViteDevServer } from '../'
import { assetUrlRE, urlToBuiltUrl } from './asset'
import MagicString from 'magic-string'
+import type {
+ ImporterReturnType,
+ Options as SassOptions,
+ Result as SassResult,
+ render as sassRender
+} from 'sass'
+import type Less from 'less'
// const debug = createDebugger('vite:css')
@@ -561,6 +568,113 @@ async function resolvePostcssConfig(
}
}
+type CssUrlReplacer = (
+ url: string,
+ importer?: string
+) => string | Promise
+const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/
+
+const UrlRewritePostcssPlugin: PluginCreator<{
+ replacer: CssUrlReplacer
+}> = (opts) => {
+ if (!opts) {
+ throw new Error('base or replace is required')
+ }
+
+ return {
+ postcssPlugin: 'vite-url-rewrite',
+ Once(root) {
+ const promises: Promise[] = []
+ root.walkDecls((decl) => {
+ if (cssUrlRE.test(decl.value)) {
+ const replacerForDecl = (rawUrl: string) => {
+ const importer = decl.source?.input.file
+ return opts.replacer(rawUrl, importer)
+ }
+ promises.push(
+ rewriteCssUrls(decl.value, replacerForDecl).then((url) => {
+ decl.value = url
+ })
+ )
+ }
+ })
+ if (promises.length) {
+ return Promise.all(promises) as any
+ }
+ }
+ }
+}
+UrlRewritePostcssPlugin.postcss = true
+
+function rewriteCssUrls(
+ css: string,
+ replacer: CssUrlReplacer
+): Promise {
+ return asyncReplace(css, cssUrlRE, async (match) => {
+ let [matched, rawUrl] = match
+ let wrap = ''
+ const first = rawUrl[0]
+ if (first === `"` || first === `'`) {
+ wrap = first
+ rawUrl = rawUrl.slice(1, -1)
+ }
+ if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) {
+ return matched
+ }
+ return `url(${wrap}${await replacer(rawUrl)}${wrap})`
+ })
+}
+
+async function processChunkCSS(
+ css: string,
+ config: ResolvedConfig,
+ pluginCtx: PluginContext,
+ isInlined: boolean,
+ minify = true
+): Promise {
+ // replace asset url references with resolved url.
+ const isRelativeBase = config.base === '' || config.base.startsWith('.')
+ css = css.replace(assetUrlRE, (_, fileId, postfix = '') => {
+ const filename = pluginCtx.getFileName(fileId) + postfix
+ if (!isRelativeBase || isInlined) {
+ // absoulte base or relative base but inlined (injected as style tag into
+ // index.html) use the base as-is
+ return config.base + filename
+ } else {
+ // relative base + extracted CSS - asset file will be in the same dir
+ return `./${path.posix.basename(filename)}`
+ }
+ })
+ if (minify && config.build.minify) {
+ css = await minifyCSS(css, config)
+ }
+ return css
+}
+
+let CleanCSS: any
+
+async function minifyCSS(css: string, config: ResolvedConfig) {
+ CleanCSS = CleanCSS || (await import('clean-css')).default
+ const res = new CleanCSS({
+ rebase: false,
+ ...config.build.cleanCssOptions
+ }).minify(css)
+
+ if (res.errors && res.errors.length) {
+ config.logger.error(chalk.red(`error when minifying css:\n${res.errors}`))
+ // TODO format this
+ throw res.errors[0]
+ }
+
+ if (res.warnings && res.warnings.length) {
+ config.logger.warn(
+ chalk.yellow(`warnings when minifying css:\n${res.warnings}`)
+ )
+ }
+
+ return res.styles
+}
+
// Preprocessor support. This logic is largely replicated from @vue/compiler-sfc
type PreprocessLang = 'less' | 'sass' | 'scss' | 'styl' | 'stylus'
@@ -594,20 +708,16 @@ function loadPreprocessor(lang: PreprocessLang) {
// .scss/.sass processor
const scss: StylePreprocessor = async (source, options, resolvers) => {
- const nodeSass = loadPreprocessor('sass')
- const finalOptions = {
+ const render = loadPreprocessor('sass').render as typeof sassRender
+ const finalOptions: SassOptions = {
...options,
data: getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
- importer(
- url: string,
- importer: string,
- done: (res: null | { file: string } | { contents: string }) => void
- ) {
+ importer(url, importer, done) {
resolvers.sass(url, importer).then((resolved) => {
if (resolved) {
- rebaseSassUrls(resolved, options.filename).then(done)
+ rebaseUrls(resolved, options.filename).then(done)
} else {
done(null)
}
@@ -616,8 +726,8 @@ const scss: StylePreprocessor = async (source, options, resolvers) => {
}
try {
- const result = await new Promise((resolve, reject) => {
- nodeSass.render(finalOptions, (err: Error | null, res: any) => {
+ const result = await new Promise((resolve, reject) => {
+ render(finalOptions, (err, res) => {
if (err) {
reject(err)
} else {
@@ -650,10 +760,14 @@ const sass: StylePreprocessor = (source, options, aliasResolver) =>
aliasResolver
)
-async function rebaseSassUrls(
+/**
+ * relative url() inside \@imported sass and less files must be rebased to use
+ * root file as base.
+ */
+async function rebaseUrls(
file: string,
rootFile: string
-): Promise<{ file: string } | { contents: string } | null> {
+): Promise {
file = path.resolve(file) // ensure os-specific flashes
// in the same dir, no need to rebase
const fileDir = path.dirname(file)
@@ -679,38 +793,32 @@ async function rebaseSassUrls(
}
// .less
-interface LessError {
- filename: string
- message: string
- line: number
- column: number
-}
-
-const less: StylePreprocessor = (source, options) => {
- const nodeLess = loadPreprocessor('less')
-
- let result: any
- let error: LessError | null = null
- nodeLess.render(
- getSource(source, options.filename, options.additionalData),
- { ...options, syncImport: true },
- (err: LessError | null, output: any) => {
- error = err
- result = output
- }
+const less: StylePreprocessor = async (source, options, resolvers) => {
+ const nodeLess = loadPreprocessor('less') as typeof Less
+ const viteResolverPlugin = createViteLessPlugin(
+ nodeLess,
+ options.filename,
+ resolvers
)
+ source = getSource(source, options.filename, options.additionalData)
- if (error) {
+ let result: Less.RenderOutput | undefined
+ try {
+ result = await nodeLess.render(source, {
+ ...options,
+ plugins: [viteResolverPlugin, ...(options.plugins || [])]
+ })
+ } catch (e) {
+ const error = e as Less.RenderError
// normalize error info
- const normalizedError: RollupError = new Error(error!.message)
+ const normalizedError: RollupError = new Error(error.message || error.type)
normalizedError.loc = {
- file: error!.filename || options.filename,
- line: error!.line,
- column: error!.column
+ file: error.filename || options.filename,
+ line: error.line,
+ column: error.column
}
return { code: '', errors: [normalizedError], deps: [] }
}
-
return {
code: result.css.toString(),
deps: result.imports,
@@ -718,6 +826,66 @@ const less: StylePreprocessor = (source, options) => {
}
}
+/**
+ * Less manager, lazy initialized
+ */
+let ViteLessManager: any
+
+function createViteLessPlugin(
+ less: typeof Less,
+ rootFile: string,
+ resolvers: CSSResolvers
+): Less.Plugin {
+ if (!ViteLessManager) {
+ ViteLessManager = class ViteManager extends less.FileManager {
+ resolvers
+ constructor(resolvers: CSSResolvers) {
+ super()
+ this.resolvers = resolvers
+ }
+ supports() {
+ return true
+ }
+ supportsSync() {
+ return false
+ }
+ async loadFile(
+ filename: string,
+ dir: string,
+ opts: any,
+ env: any
+ ): Promise {
+ const resolved = await this.resolvers.less(
+ filename,
+ path.join(dir, '*')
+ )
+ if (resolved) {
+ const result = await rebaseUrls(resolved, rootFile)
+ let contents
+ if (result && 'contents' in result) {
+ contents = result.contents
+ } else {
+ contents = fs.readFileSync(resolved, 'utf-8')
+ }
+ return {
+ filename: path.resolve(resolved),
+ contents
+ }
+ } else {
+ return super.loadFile(filename, dir, opts, env)
+ }
+ }
+ }
+ }
+
+ return {
+ install(_, pluginManager) {
+ pluginManager.addFileManager(new ViteLessManager(resolvers))
+ },
+ minVersion: [3, 0, 0]
+ }
+}
+
// .styl
const styl: StylePreprocessor = (source, options) => {
const nodeStylus = loadPreprocessor('stylus')
@@ -739,7 +907,7 @@ function getSource(
source: string,
filename: string,
additionalData?: string | ((source: string, filename: string) => string)
-) {
+): string {
if (!additionalData) return source
if (typeof additionalData === 'function') {
return additionalData(source, filename)
@@ -754,110 +922,3 @@ const preProcessors = {
styl,
stylus: styl
}
-
-type CssUrlReplacer = (
- url: string,
- importer?: string
-) => string | Promise
-const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/
-
-const UrlRewritePostcssPlugin: PluginCreator<{
- replacer: CssUrlReplacer
-}> = (opts) => {
- if (!opts) {
- throw new Error('base or replace is required')
- }
-
- return {
- postcssPlugin: 'vite-url-rewrite',
- Once(root) {
- const promises: Promise[] = []
- root.walkDecls((decl) => {
- if (cssUrlRE.test(decl.value)) {
- const replacerForDecl = (rawUrl: string) => {
- const importer = decl.source?.input.file
- return opts.replacer(rawUrl, importer)
- }
- promises.push(
- rewriteCssUrls(decl.value, replacerForDecl).then((url) => {
- decl.value = url
- })
- )
- }
- })
- if (promises.length) {
- return Promise.all(promises) as any
- }
- }
- }
-}
-UrlRewritePostcssPlugin.postcss = true
-
-function rewriteCssUrls(
- css: string,
- replacer: CssUrlReplacer
-): Promise {
- return asyncReplace(css, cssUrlRE, async (match) => {
- let [matched, rawUrl] = match
- let wrap = ''
- const first = rawUrl[0]
- if (first === `"` || first === `'`) {
- wrap = first
- rawUrl = rawUrl.slice(1, -1)
- }
- if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) {
- return matched
- }
- return `url(${wrap}${await replacer(rawUrl)}${wrap})`
- })
-}
-
-async function processChunkCSS(
- css: string,
- config: ResolvedConfig,
- pluginCtx: PluginContext,
- isInlined: boolean,
- minify = true
-): Promise {
- // replace asset url references with resolved url.
- const isRelativeBase = config.base === '' || config.base.startsWith('.')
- css = css.replace(assetUrlRE, (_, fileId, postfix = '') => {
- const filename = pluginCtx.getFileName(fileId) + postfix
- if (!isRelativeBase || isInlined) {
- // absoulte base or relative base but inlined (injected as style tag into
- // index.html) use the base as-is
- return config.base + filename
- } else {
- // relative base + extracted CSS - asset file will be in the same dir
- return `./${path.posix.basename(filename)}`
- }
- })
- if (minify && config.build.minify) {
- css = await minifyCSS(css, config)
- }
- return css
-}
-
-let CleanCSS: any
-
-async function minifyCSS(css: string, config: ResolvedConfig) {
- CleanCSS = CleanCSS || (await import('clean-css')).default
- const res = new CleanCSS({
- rebase: false,
- ...config.build.cleanCssOptions
- }).minify(css)
-
- if (res.errors && res.errors.length) {
- config.logger.error(chalk.red(`error when minifying css:\n${res.errors}`))
- // TODO format this
- throw res.errors[0]
- }
-
- if (res.warnings && res.warnings.length) {
- config.logger.warn(
- chalk.yellow(`warnings when minifying css:\n${res.warnings}`)
- )
- }
-
- return res.styles
-}
diff --git a/yarn.lock b/yarn.lock
index a9742896ab5c31..b8946d17bb8d12 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1000,6 +1000,11 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
+"@types/less@^3.0.2":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.2.tgz#2761d477678c8374cb9897666871662eb1d1115e"
+ integrity sha512-62vfe65cMSzYaWmpmhqCMMNl0khen89w57mByPi1OseGfcV/LV03fO8YVrNj7rFQsRWNJo650WWyh6m7p8vZmA==
+
"@types/mime@^2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
@@ -1062,6 +1067,13 @@
dependencies:
"@types/node" "*"
+"@types/sass@^1.16.0":
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.16.0.tgz#b41ac1c17fa68ffb57d43e2360486ef526b3d57d"
+ integrity sha512-2XZovu4NwcqmtZtsBR5XYLw18T8cBCnU2USFHTnYLLHz9fkhnoEMoDsqShJIOFsFhn5aJHjweiUUdTrDGujegA==
+ dependencies:
+ "@types/node" "*"
+
"@types/semver@^7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb"
@@ -2562,6 +2574,13 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1:
dependencies:
ms "2.1.2"
+debug@^3.2.6:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+ integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+ dependencies:
+ ms "^2.1.1"
+
decamelize-keys@^1.0.0, decamelize-keys@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
@@ -3769,7 +3788,7 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
-iconv-lite@0.4.24:
+iconv-lite@0.4.24, iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -4847,6 +4866,23 @@ less@^3.13.0:
native-request "^1.0.5"
source-map "~0.6.0"
+less@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/less/-/less-4.1.0.tgz#a12708d1951239db1c9d7eaa405f1ebac9a75b8d"
+ integrity sha512-w1Ag/f34g7LwtQ/sMVSGWIyZx+gG9ZOAEtyxeX1fG75is6BMyC2lD5kG+1RueX7PkAvlQBm2Lf2aN2j0JbVr2A==
+ dependencies:
+ copy-anything "^2.0.1"
+ parse-node-version "^1.0.1"
+ tslib "^1.10.0"
+ optionalDependencies:
+ errno "^0.1.1"
+ graceful-fs "^4.1.2"
+ image-size "~0.5.0"
+ make-dir "^2.1.0"
+ mime "^1.4.1"
+ needle "^2.5.2"
+ source-map "~0.6.0"
+
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -5399,6 +5435,11 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+ms@^2.1.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
@@ -5431,6 +5472,15 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+needle@^2.5.2:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe"
+ integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==
+ dependencies:
+ debug "^3.2.6"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@@ -5799,6 +5849,11 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
+parse-node-version@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
+ integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
+
parse5@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
@@ -6819,6 +6874,18 @@ sass@^1.30.0:
dependencies:
chokidar ">=2.0.0 <4.0.0"
+sass@^1.32.5:
+ version "1.32.5"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.5.tgz#2882d22ad5748c05fa9bff6c3b0ffbc4f4b9e1dc"
+ integrity sha512-kU1yJ5zUAmPxr7f3q0YXTAd1oZjSR1g3tYyv+xu0HZSl5JiNOaE987eiz7wCUvbm4I9fGWGU2TgApTtcP4GMNQ==
+ dependencies:
+ chokidar ">=2.0.0 <4.0.0"
+
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
saxes@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"