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"