Эх сурвалжийг харах

feat(tauri) initial work for the CLI (#20)

* feat(tauri) initial work for the CLI

* feat(build) read dist path from env

* feat(tauri) proton.js entry

* feat(tauri) threeshake fs

* feat(injector) check if tauri dir exists

* feat(injector) don't show success message if dir exists

* chore(injector) cleanup fs imports
Lucas Fernandes Nogueira 6 жил өмнө
parent
commit
a0f410dcff

+ 1 - 4
lib/rust/build.rs

@@ -3,11 +3,8 @@ extern crate includedir_codegen;
 use includedir_codegen::Compression;
 use std::env;
 
-static CARGOENV: &str = "cargo:rustc-env=";
-
 fn main() {
-  let dist_path = format!("{}/../../../../{}", env::var("OUT_DIR").unwrap(), "compiled-web");
-  println!("{}TAURI_DIST_DIR={}", CARGOENV, dist_path);
+  let dist_path = env::var("TAURI_DIST_DIR").unwrap();
   includedir_codegen::start("ASSETS")
     .dir(dist_path, Compression::None)
     .build("data.rs")

+ 337 - 0
lib/tauri.js

@@ -0,0 +1,337 @@
+<%
+  if (typeof(confName) === 'undefined') {
+    confName = 'quasar.conf.js'
+  }
+%>
+/**
+ * THIS FILE IS GENERATED AUTOMATICALLY.
+ * DO NOT EDIT.
+ *
+ * Please whitelist these API functions in <% confName %>
+ *
+ **/
+
+/**
+ * @module tauri
+ * @description This API interface makes powerful interactions available
+ * to be run on client side applications. They are opt-in features, and
+ * must be enabled in <% confName %>
+ *
+ * Each binding MUST provide these interfaces in order to be compliant,
+ * and also whitelist them based upon the developer's settings.
+ */
+
+function s4() {
+  return Math.floor((1 + Math.random()) * 0x10000)
+    .toString(16)
+    .substring(1)
+}
+
+const uid = function () {
+  return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+    s4() + '-' + s4() + s4() + s4()
+}
+
+<% if (ctx.dev) { %>
+/**
+ * @name __whitelistWarning
+ * @description Present a stylish warning to the developer that their API
+ * call has not been whitelisted in <% confName %>
+ * @param {String} func - function name to warn
+ * @private
+ */
+const __whitelistWarning = function (func) {
+  console.warn('%c[Tauri] Danger \ntauri.' + func + ' not whitelisted 💣\n%c\nAdd to <% confName %>: \n\ntauri: \n  whitelist: { \n    ' + func + ': true \n\nReference: https://quasar.dev/quasar-cli/creating-tauri-apps/api#' + func , 'background: red; color: white; font-weight: 800; padding: 2px; font-size:1.5em', ' ')
+}
+<% } %>
+
+/**
+ * @name __reject
+ * @description is a private promise used to deflect un-whitelisted tauri API calls
+ * Its only purpose is to maintain thenable structure in client code without
+ * breaking the application
+ *  * @type {Promise<any>}
+ * @private
+ */
+const __reject = new Promise((reject) => { reject })
+
+export default class Tauri {
+<% if (ctx.dev) { %>
+  /**
+   * @name invoke
+   * @description Calls a Quasar Core feature, such as setTitle
+   * @param {Object} args
+   */
+<% } %>
+  static invoke (args) {
+  Object.freeze(args)
+    external.invoke(JSON.stringify(args))
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name addEventListener
+   * @description Add an event listener to Tauri back end
+   * @param {String} event
+   * @param {Function} handler
+   * @param {Boolean} once
+   */
+<% } %>
+  static addEventListener(event, handler, once = false) {
+    this.invoke({
+      cmd: 'addEventListener',
+      event,
+      handler: this.transformCallback(handler, once),
+      once
+    })
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name emit
+   * @description Emits an event to the Tauri back end
+   * @param {String} event
+   * @param {Object} payload
+   */
+<% } %>
+  static emit(event, payload) {
+  this.invoke({
+    cmd: 'emit',
+    event,
+    payload
+  })
+}
+
+<% if (ctx.dev) { %>
+  /**
+   * @name transformCallback
+   * @description Registers a callback with a uid
+   * @param {Function} callback
+   * @param {Boolean} once
+   * @returns {*}
+   */
+<% } %>
+  static transformCallback (callback, once = true) {
+    const identifier = Object.freeze(uid())
+    window[identifier] = (result) => {
+      if (once) {
+        delete window[identifier]
+      }
+      return callback && callback(result)
+    }
+    return identifier
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name promisified
+   * @description Turns a request into a chainable promise
+   * @param {Object} args
+   * @returns {Promise<any>}
+   */
+<% } %>
+  static promisified (args) {
+    return new Promise((resolve, reject) => {
+      this.invoke({
+        callback: this.transformCallback(resolve),
+        error: this.transformCallback(reject),
+        ...args
+      })
+    })
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name readTextFile
+   * @description Accesses a non-binary file on the user's filesystem
+   * and returns the content. Permissions based on the app's PID owner
+   * @param {String} path
+   * @returns {*|Promise<any>|Promise}
+   */
+<% } %>
+  static readTextFile (path) {
+  <% if (tauri.whitelist.readTextFile === true || tauri.whitelist.all === true) { %>
+    Object.freeze(path)
+    return this.promisified({ cmd: 'readTextFile', path })
+      <% } else { %>
+  <% if (ctx.dev) { %>
+      __whitelistWarning('readTextFile')
+      <% } %>
+    return __reject
+      <% } %>
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name readBinaryFile
+   * @description Accesses a binary file on the user's filesystem
+   * and returns the content. Permissions based on the app's PID owner
+   * @param {String} path
+   * @returns {*|Promise<any>|Promise}
+   */
+<% } %>
+  static readBinaryFile (path) {
+  <% if (tauri.whitelist.readBinaryFile === true || tauri.whitelist.all === true) { %>
+    Object.freeze(path)
+    return this.promisified({ cmd: 'readBinaryFile', path })
+      <% } else { %>
+  <% if (ctx.dev) { %>
+      __whitelistWarning('readBinaryFile')
+      <% } %>
+    return __reject
+      <% } %>
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name writeFile
+   * @description Write a file to the Local Filesystem.
+   * Permissions based on the app's PID owner
+   * @param {Object} cfg
+   * @param {String} cfg.file
+   * @param {String|Binary} cfg.contents
+   */
+<% } %>
+  static writeFile (cfg) {
+  Object.freeze(cfg)
+  <% if (tauri.whitelist.writeFile === true || tauri.whitelist.all === true) { %>
+    this.invoke({ cmd: 'writeFile', file: cfg.file, contents: cfg.contents })
+    <% } else { %>
+  <% if (ctx.dev) { %>
+      __whitelistWarning('writeFile')
+      <% } %>
+    return __reject
+      <% } %>  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name listFiles
+   * @description Get the files in a path.
+   * Permissions based on the app's PID owner
+   * @param {String} path
+   * @returns {*|Promise<any>|Promise}
+   */
+<% } %>
+  static listFiles (path) {
+  <% if (tauri.whitelist.listFiles === true || tauri.whitelist.all === true) { %>
+    Object.freeze(path)
+    return this.promisified({ cmd: 'listFiles', path })
+      <% } else { %>
+  <% if (ctx.dev) { %>
+      __whitelistWarning('listFiles')
+      <% } %>
+    return __reject
+      <% } %>
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name listDirs
+   * @description Get the directories in a path.
+   * Permissions based on the app's PID owner
+   * @param {String} path
+   * @returns {*|Promise<any>|Promise}
+   */
+<% } %>
+  static listDirs (path) {
+  <% if (tauri.whitelist.listDirs === true || tauri.whitelist.all === true) { %>
+    Object.freeze(path)
+    return this.promisified({ cmd: 'listDirs', path })
+      <% } else { %>
+  <% if (ctx.dev) { %>
+      __whitelistWarning('listDirs')
+      <% } %>
+    return __reject
+      <% } %>
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name setTitle
+   * @description Set the application's title
+   * @param {String} title
+   */
+<% } %>
+  static setTitle (title) {
+    <% if (tauri.whitelist.setTitle === true || tauri.whitelist.all === true) { %>
+    Object.freeze(title)
+    this.invoke({ cmd: 'setTitle', title })
+      <% } else { %>
+  <% if (ctx.dev) { %>
+      __whitelistWarning('setTitle')
+      <% } %>
+    return __reject
+      <% } %>
+  }
+
+  <% if (ctx.dev) { %>
+  /**
+   * @name open
+   * @description Open an URI
+   * @param {String} uri
+   */
+<% } %>
+  static open (uri) {
+    <% if (tauri.whitelist.open === true || tauri.whitelist.all === true) { %>
+    Object.freeze(uri)
+    this.invoke({ cmd: 'open', uri })
+      <% } else { %>
+  <% if (ctx.dev) { %>
+      __whitelistWarning('open')
+      <% } %>
+    return __reject
+      <% } %>
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name execute
+   * @description Execute a program with arguments.
+   * Permissions based on the app's PID owner
+   * @param {String} command
+   * @param {String|Array} args
+   * @returns {*|Promise<any>|Promise}
+   */
+<% } %>
+  static execute (command, args) {
+    <% if (tauri.whitelist.execute === true || tauri.whitelist.all === true) { %>
+    Object.freeze(command)
+    if (typeof args === 'string' || typeof args === 'object') {
+      Object.freeze(args)
+    }
+    return this.promisified({ cmd: 'execute', command, args: typeof (args) === 'string' ? [args] : args })
+  <% } else { %>
+  <% if (ctx.dev) { %>
+    __whitelistWarning('execute')
+    <% } %>
+    return __reject
+      <% } %>
+  }
+
+<% if (ctx.dev) { %>
+  /**
+   * @name bridge
+   * @description Securely pass a message to the backend.
+   * @example
+   *  this.$q.tauri.bridge('QBP/1/ping/client-1', 'pingback')
+   * @param {String} command - a compressed, slash-delimited and
+   * versioned API call to the backend.
+   * @param {String|Object}payload
+   * @returns {*|Promise<any>|Promise}
+   */
+<% } %>
+  static bridge (command, payload) {
+<% if (tauri.whitelist.bridge === true || tauri.whitelist.all === true) { %>
+    Object.freeze(command)
+    if (typeof payload === 'string' || typeof payload === 'object') {
+      Object.freeze(payload)
+    }
+    return this.promisified({ cmd: 'bridge', command, payload: typeof (payload) === 'object' ? [payload] : payload })
+<% } else { %>
+<% if (ctx.dev) { %>
+    __whitelistWarning('bridge')
+<% } %>
+      return __reject
+<% } %>
+  }
+}

+ 17 - 1
package.json

@@ -2,7 +2,10 @@
   "name": "@quasar/tauri",
   "version": "1.0.0-alpha.1",
   "description": "Multi-binding collection of libraries and templates for building Tauri",
-  "main": "templates/index.js",
+  "main": "tauri/lib.js",
+  "bin": {
+    "tauri": "tauri/bin/tauri.js"
+  },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
@@ -23,5 +26,18 @@
     "node": ">= 10.16.0",
     "npm": ">= 6.6.0",
     "yarn": ">= 1.17.3"
+  },
+  "dependencies": {
+    "@iarna/toml": "^2.2.3",
+    "chalk": "^2.4.2",
+    "chokidar": "^3.0.2",
+    "cross-spawn": "^6.0.5",
+    "fast-glob": "^3.0.4",
+    "fs-extra": "^8.1.0",
+    "lodash.debounce": "^4.0.8",
+    "lodash.template": "^4.5.0",
+    "minimist": "^1.2.0",
+    "ms": "^2.1.2",
+    "webpack-merge": "^4.2.1"
   }
 }

+ 42 - 0
tauri/bin/tauri-build.js

@@ -0,0 +1,42 @@
+const
+  parseArgs = require('minimist'),
+  { writeFileSync } = require('fs-extra'),
+  path = require('path')
+
+const argv = parseArgs(process.argv.slice(2), {
+  alias: {
+    h: 'help',
+    d: 'debug'
+  },
+  boolean: ['h', 'd']
+})
+
+if (argv.help) {
+  console.log(`
+  Description
+    Tauri build.
+  Usage
+    $ tauri build
+  Options
+    --help, -h     Displays this message
+  `)
+  process.exit(0)
+}
+
+const appPaths = require('../helpers/app-paths'),
+  Runner = require('../runner'),
+  Injector = require('../injector'),
+  tauri = new Runner(appPaths),
+  injector = new Injector(appPaths),
+  tauriConfig = require('../helpers/tauri-config')({
+    ctx: {
+      debug: argv.debug
+    }
+  })
+const {bundle, ...cfg} = tauriConfig.tauri,
+  cfgDir = injector.configDir()
+writeFileSync(path.join(cfgDir, 'config.json'), JSON.stringify(cfg))
+writeFileSync(path.join(cfgDir, 'bundle.json'), JSON.stringify(bundle))
+
+require('../helpers/generator')(tauriConfig)
+tauri.build(tauriConfig)

+ 44 - 0
tauri/bin/tauri-dev.js

@@ -0,0 +1,44 @@
+const
+  parseArgs = require('minimist'),
+  path = require('path'),
+  { writeFileSync } = require('fs-extra')
+
+const argv = parseArgs(process.argv.slice(2), {
+  alias: {
+    h: 'help'
+  },
+  boolean: ['h']
+})
+
+if (argv.help) {
+  console.log(`
+  Description
+    Tauri dev.
+  Usage
+    $ tauri dev
+  Options
+    --help, -h     Displays this message
+  `)
+  process.exit(0)
+}
+
+const appPaths = require('../helpers/app-paths'),
+  Runner = require('../runner'),
+  Injector = require('../injector'),
+  tauri = new Runner(appPaths),
+  injector = new Injector(appPaths),
+  tauriConfig = require('../helpers/tauri-config')({
+    ctx: {
+      debug: true
+    }
+  })
+
+const { bundle, ...cfg } = tauriConfig.tauri,
+  cfgDir = injector.configDir()
+
+writeFileSync(path.join(cfgDir, 'config.json'), JSON.stringify(cfg))
+writeFileSync(path.join(cfgDir, 'bundle.json'), JSON.stringify(bundle))
+
+require('../helpers/generator')(tauriConfig)
+
+tauri.run(tauriConfig)

+ 29 - 0
tauri/bin/tauri-init.js

@@ -0,0 +1,29 @@
+const
+    parseArgs = require('minimist'),
+    appPaths = require('../helpers/app-paths'),
+    log = require('../helpers/logger')('app:tauri')
+
+const argv = parseArgs(process.argv.slice(2), {
+    alias: {
+        h: 'help'
+    },
+    boolean: ['h']
+})
+
+if (argv.help) {
+    console.log(`
+  Description
+    Inits the Tauri template.
+  Usage
+    $ tauri init
+  Options
+    --help, -h     Displays this message
+  `)
+    process.exit(0)
+}
+
+const Injector = require('../injector'),
+  injector = new Injector(appPaths)
+if (injector.injectTemplate()) {
+  log('Tauri template successfully installed')
+}

+ 22 - 0
tauri/bin/tauri.js

@@ -0,0 +1,22 @@
+#!/usr/bin/env node
+
+const cmds = ['init', 'dev', 'build', 'help']
+
+const cmd = process.argv[2]
+if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') {
+  console.log(`
+  Description
+    Tauri CLI.
+  Usage
+    $ tauri ${cmds.join('|')}
+  Options
+    --help, -h     Displays this message
+  `)
+  process.exit(0)
+}
+if (cmds.includes(cmd)) {
+   process.argv.splice(2, 1)
+   require(`./tauri-${cmd}`)
+} else {
+  console.log(`Invalid command ${cmd}. Use one of ${cmds.join(',')}.`)
+}

+ 38 - 0
tauri/helpers/app-paths.js

@@ -0,0 +1,38 @@
+const
+  { existsSync } = require('fs'),
+  path = require('path'),
+  resolve = path.resolve,
+  join = path.join
+
+function getAppDir() {
+  let dir = process.cwd()
+
+  while (dir.length && dir[dir.length - 1] !== path.sep) {
+    if (existsSync(join(dir, 'tauri.conf.js'))) {
+      return dir
+    }
+
+    dir = path.normalize(join(dir, '..'))
+  }
+
+  const
+    logger = require('./logger')
+  warn = logger('app:paths', 'red')
+
+  warn(`⚠️  Error. This command must be executed inside a Tauri project folder.`)
+  warn()
+  process.exit(1)
+}
+
+const appDir = getAppDir(),
+  tauriDir = resolve(appDir, 'src-tauri')
+
+module.exports = {
+  appDir,
+  tauriDir,
+
+  resolve: {
+    app: dir => join(appDir, dir),
+    tauri: dir => join(tauriDir, dir)
+  }
+}

+ 13 - 0
tauri/helpers/generator.js

@@ -0,0 +1,13 @@
+const compileTemplate = require('lodash.template'),
+  { readFileSync, writeFileSync } = require('fs'),
+  appPaths = require('./app-paths'),
+  path = require('path')
+
+module.exports = cfg => {
+  const apiTemplate = readFileSync(path.resolve(__dirname, '../../lib/tauri.js'), 'utf-8')
+  const apiContent = compileTemplate(apiTemplate)({
+    ...cfg,
+    confName: 'tauri.conf.js'
+  })
+  writeFileSync(appPaths.resolve.tauri('tauri.js'), apiContent, 'utf-8')
+}

+ 21 - 0
tauri/helpers/logger.js

@@ -0,0 +1,21 @@
+const
+  ms = require('ms'),
+  chalk = require('chalk')
+
+let prevTime
+
+module.exports = function (banner, color = 'green') {
+  return function (msg) {
+    const
+      curr = +new Date(),
+      diff = curr - (prevTime || curr)
+
+    prevTime = curr
+
+    if (msg) {
+      console.log(` ${chalk[color](banner)} ${msg} ${chalk.green(`+${ms(diff)}`)}`)
+    } else {
+      console.log()
+    }
+  }
+}

+ 15 - 0
tauri/helpers/on-shutdown.js

@@ -0,0 +1,15 @@
+module.exports = function (fn) {
+  const cleanup = () => {
+    try {
+      fn()
+    } finally {
+      process.exit()
+    }
+  }
+
+  process.on('exit', cleanup)
+  process.on('SIGINT', cleanup)
+  process.on('SIGTERM', cleanup)
+  process.on('SIGHUP', cleanup)
+  process.on('SIGBREAK', cleanup)
+}

+ 62 - 0
tauri/helpers/spawn.js

@@ -0,0 +1,62 @@
+const
+  logger = require('./logger'),
+  log = logger('app:spawn'),
+  warn = logger('app:spawn', 'red'),
+  crossSpawn = require('cross-spawn')
+
+/*
+ Returns pid, takes onClose
+ */
+module.exports.spawn = function (cmd, params, cwd, onClose) {
+  log(`Running "${cmd} ${params.join(' ')}"`)
+  log()
+
+  const runner = crossSpawn(
+    cmd,
+    params, {
+      stdio: 'inherit',
+      stdout: 'inherit',
+      stderr: 'inherit',
+      cwd
+    }
+  )
+
+  runner.on('close', code => {
+    log()
+    if (code) {
+      log(`Command "${cmd}" failed with exit code: ${code}`)
+    }
+
+    onClose && onClose(code)
+  })
+
+  return runner.pid
+}
+
+/*
+ Returns nothing, takes onFail
+ */
+module.exports.spawnSync = function (cmd, params, cwd, onFail) {
+  log(`[sync] Running "${cmd} ${params.join(' ')}"`)
+  log()
+
+  const runner = crossSpawn.sync(
+    cmd,
+    params, {
+      stdio: 'inherit',
+      stdout: 'inherit',
+      stderr: 'inherit',
+      cwd
+    }
+  )
+
+  if (runner.status || runner.error) {
+    warn()
+    warn(`⚠️  Command "${cmd}" failed with exit code: ${runner.status}`)
+    if (runner.status === null) {
+      warn(`⚠️  Please globally install "${cmd}"`)
+    }
+    onFail && onFail()
+    process.exit(1)
+  }
+}

+ 29 - 0
tauri/helpers/tauri-config.js

@@ -0,0 +1,29 @@
+const appPaths = require('./app-paths'),
+  merge = require('webpack-merge')
+
+module.exports = cfg => {
+  const tauriConf = require(appPaths.resolve.app('tauri.conf.js'))(cfg.ctx)
+  const config = merge({
+    build: {},
+    ctx: {},
+    tauri: {
+      embeddedServer: {
+        active: true
+      },
+      bundle: {
+        active: true
+      },
+      whitelist: {},
+      window: {
+        title: require(appPaths.resolve.app('package.json')).productName
+      },
+      security: {
+        csp: 'default-src data: filesystem: ws: http: https: \'unsafe-eval\' \'unsafe-inline\''
+      }
+    }
+  }, tauriConf, cfg)
+
+  process.env.TAURI_DIST_DIR = appPaths.resolve.app(config.build.distDir)
+
+  return config
+}

+ 41 - 0
tauri/injector.js

@@ -0,0 +1,41 @@
+const { copySync, renameSync, existsSync, mkdirSync } = require('fs-extra'),
+  path = require('path')
+
+class TauriInjector {
+  constructor(appPaths) {
+    this.appPaths = appPaths
+  }
+
+  configDir() {
+    return path.resolve(__dirname, '..')
+  }
+
+  injectTemplate() {
+    if (existsSync(this.appPaths.tauriDir)) {
+      console.log(`Tauri dir (${this.appPaths.tauriDir}) not empty.`)
+      return false
+    }
+    mkdirSync(this.appPaths.tauriDir)
+    copySync(path.resolve(__dirname, '../templates/rust'), this.appPaths.tauriDir)
+    const files = require('fast-glob').sync(['**/_*'], {
+      cwd: this.appPaths.tauriDir
+    })
+    for (const rawPath of files) {
+      const targetRelativePath = rawPath.split('/').map(name => {
+        // dotfiles are ignored when published to npm, therefore in templates
+        // we need to use underscore instead (e.g. "_gitignore")
+        if (name.charAt(0) === '_' && name.charAt(1) !== '_') {
+          return `.${name.slice(1)}`
+        }
+        if (name.charAt(0) === '_' && name.charAt(1) === '_') {
+          return `${name.slice(1)}`
+        }
+        return name
+      }).join('/')
+      renameSync(this.appPaths.resolve.tauri(rawPath), this.appPaths.resolve.tauri(targetRelativePath))
+    }
+    return true
+  }
+}
+
+module.exports = TauriInjector

+ 9 - 0
tauri/lib.js

@@ -0,0 +1,9 @@
+const TauriRunner = require('./runner'),
+  TauriInjector = require('./injector'),
+  path = require('path')
+
+module.exports = {
+  runner: TauriRunner,
+  injector: TauriInjector,
+  apiTemplatePath: path.resolve(__dirname, '../lib/tauri.js')
+}

+ 189 - 0
tauri/runner.js

@@ -0,0 +1,189 @@
+const
+  chokidar = require('chokidar'),
+  debounce = require('lodash.debounce')
+
+const
+  { spawn } = require('./helpers/spawn'),
+  log = require('./helpers/logger')('app:tauri')
+  onShutdown = require('./helpers/on-shutdown'),
+  { readFileSync, writeFileSync } = require('fs-extra')
+
+class TauriRunner {
+  constructor(appPaths) {
+    this.appPaths = appPaths
+    this.pid = 0
+    this.tauriWatcher = null
+
+    onShutdown(() => {
+      this.stop()
+    })
+  }
+
+  async run(cfg) {
+    const
+      url = cfg.build.APP_URL
+
+    if (this.pid) {
+      if (this.url !== url) {
+        await this.stop()
+      } else {
+        return
+      }
+    }
+
+    this.__manipulateToml(toml => {
+      this.__whitelistApi(cfg, toml)
+    })
+
+    this.url = url
+
+    const args = ['--url', url]
+
+    const startDevTauri = () => {
+      return this.__runCargoCommand({
+        cargoArgs: ['run', '--features', 'dev'],
+        extraArgs: args
+      })
+    }
+
+    // Start watching for tauri app changes
+    this.tauriWatcher = chokidar
+      .watch([
+        this.appPaths.resolve.tauri('src'),
+        this.appPaths.resolve.tauri('Cargo.toml'),
+        this.appPaths.resolve.tauri('build.rs')
+      ], {
+        watchers: {
+          chokidar: {
+            ignoreInitial: true
+          }
+        }
+      })
+      .on('change', debounce(async () => {
+        await this.__stopCargo()
+        startDevTauri()
+      }, 1000))
+
+    return startDevTauri()
+  }
+
+  async build(cfg) {
+    this.__manipulateToml(toml => {
+      this.__whitelistApi(cfg, toml)
+    })
+
+    const features = []
+    if (cfg.tauri.embeddedServer.active) {
+      features.push('embedded-server')
+    }
+
+    const buildFn = target => this.__runCargoCommand({
+      cargoArgs: [cfg.tauri.bundle.active ? 'tauri-bundle' : 'build']
+        .concat(features.length ? ['--features', ...features] : [])
+        .concat(cfg.ctx.debug ? [] : ['--release'])
+        .concat(target ? ['--target', target] : [])
+    })
+
+    if (cfg.ctx.debug || !cfg.ctx.targetName) {
+      // on debug mode or if not arget specified,
+      // build only for the current platform
+      return buildFn()
+    }
+
+    const targets = cfg.ctx.target.split(',')
+
+    for (const target of targets) {
+      await buildFn(target)
+    }
+  }
+
+  stop() {
+    return new Promise((resolve, reject) => {
+      this.tauriWatcher && this.tauriWatcher.close()
+      this.__stopCargo().then(resolve)
+    })
+  }
+
+  __runCargoCommand({
+    cargoArgs,
+    extraArgs
+  }) {
+    return new Promise(resolve => {
+      this.pid = spawn(
+        'cargo',
+
+        extraArgs ?
+        cargoArgs.concat(['--']).concat(extraArgs) :
+        cargoArgs,
+
+        this.appPaths.tauriDir,
+
+        code => {
+          if (code) {
+            warn()
+            warn(`⚠️  [FAIL] Cargo CLI has failed`)
+            warn()
+            process.exit(1)
+          }
+
+          if (this.killPromise) {
+            this.killPromise()
+            this.killPromise = null
+          } else { // else it wasn't killed by us
+            warn()
+            warn('Cargo process was killed. Exiting...')
+            warn()
+            process.exit(0)
+          }
+        }
+      )
+
+      resolve()
+    })
+  }
+
+  __stopCargo() {
+    const pid = this.pid
+
+    if (!pid) {
+      return Promise.resolve()
+    }
+
+    log('Shutting down tauri process...')
+    this.pid = 0
+
+    return new Promise((resolve, reject) => {
+      this.killPromise = resolve
+      process.kill(pid)
+    })
+  }
+
+  __manipulateToml(callback) {
+    const toml = require('@iarna/toml'),
+      tomlPath = this.appPaths.resolve.tauri('Cargo.toml'),
+      tomlFile = readFileSync(tomlPath),
+      tomlContents = toml.parse(tomlFile)
+
+    callback(tomlContents)
+
+    const output = toml.stringify(tomlContents)
+    writeFileSync(tomlPath, output)
+  }
+
+  __whitelistApi(cfg, tomlContents) {
+    if (!tomlContents.dependencies.tauri.features) {
+      tomlContents.dependencies.tauri.features = []
+    }
+
+    if (cfg.tauri.whitelist.all) {
+      if (!tomlContents.dependencies.tauri.features.includes('all-api')) {
+        tomlContents.dependencies.tauri.features.push('all-api')
+      }
+    } else {
+      const whitelist = Object.keys(cfg.tauri.whitelist).filter(w => cfg.tauri.whitelist[w] === true)
+      tomlContents.dependencies.tauri.features = whitelist.concat(tomlContents.dependencies.tauri.features.filter(f => f !== 'api' && cfg.tauri.whitelist[f] !== true))
+    }
+  }
+}
+
+module.exports = TauriRunner

+ 3 - 1
templates/rust/_gitignore

@@ -7,4 +7,6 @@
 Cargo.lock
 
 # These are backup files generated by rustfmt
-**/*.rs.bk
+**/*.rs.bk
+
+tauri.js

+ 341 - 0
yarn.lock

@@ -0,0 +1,341 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@iarna/toml@^2.2.3":
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.3.tgz#f060bf6eaafae4d56a7dac618980838b0696e2ab"
+  integrity sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==
+
+"@nodelib/fs.scandir@2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz#7fa8fed654939e1a39753d286b48b4836d00e0eb"
+  integrity sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.1"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.1", "@nodelib/fs.stat@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz#814f71b1167390cfcb6a6b3d9cdeb0951a192c14"
+  integrity sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==
+
+"@nodelib/fs.walk@^1.2.1":
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz#6a6450c5e17012abd81450eb74949a4d970d2807"
+  integrity sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.1"
+    fastq "^1.6.0"
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+  dependencies:
+    color-convert "^1.9.0"
+
+anymatch@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.0.3.tgz#2fb624fe0e84bccab00afee3d0006ed310f22f09"
+  integrity sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+binary-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
+  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+
+braces@^3.0.1, braces@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+chalk@^2.4.2:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chokidar@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681"
+  integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==
+  dependencies:
+    anymatch "^3.0.1"
+    braces "^3.0.2"
+    glob-parent "^5.0.0"
+    is-binary-path "^2.1.0"
+    is-glob "^4.0.1"
+    normalize-path "^3.0.0"
+    readdirp "^3.1.1"
+  optionalDependencies:
+    fsevents "^2.0.6"
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+cross-spawn@^6.0.5:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+fast-glob@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602"
+  integrity sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.1"
+    "@nodelib/fs.walk" "^1.2.1"
+    glob-parent "^5.0.0"
+    is-glob "^4.0.1"
+    merge2 "^1.2.3"
+    micromatch "^4.0.2"
+
+fastq@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2"
+  integrity sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==
+  dependencies:
+    reusify "^1.0.0"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+fs-extra@^8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+  integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+  dependencies:
+    graceful-fs "^4.2.0"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
+fsevents@^2.0.6:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a"
+  integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==
+
+glob-parent@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954"
+  integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==
+  dependencies:
+    is-glob "^4.0.1"
+
+graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
+  integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+is-binary-path@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-glob@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
+lodash._reinterpolate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
+  integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+
+lodash.debounce@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+  integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+
+lodash.template@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
+  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
+  dependencies:
+    lodash._reinterpolate "^3.0.0"
+    lodash.templatesettings "^4.0.0"
+
+lodash.templatesettings@^4.0.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
+  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
+  dependencies:
+    lodash._reinterpolate "^3.0.0"
+
+lodash@^4.17.5:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+merge2@^1.2.3:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3"
+  integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==
+
+micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
+minimist@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+
+ms@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+normalize-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+picomatch@^2.0.4, picomatch@^2.0.5:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6"
+  integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==
+
+readdirp@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.2.tgz#fa85d2d14d4289920e4671dead96431add2ee78a"
+  integrity sha512-8rhl0xs2cxfVsqzreYCvs8EwBfn/DhVdqtoLmw19uI3SC5avYX9teCurlErfpPXGmYtMHReGaP2RsLnFvz/lnw==
+  dependencies:
+    picomatch "^2.0.4"
+
+reusify@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+run-parallel@^1.1.9:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
+  integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
+
+semver@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+  dependencies:
+    has-flag "^3.0.0"
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+universalify@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+webpack-merge@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.1.tgz#5e923cf802ea2ace4fd5af1d3247368a633489b4"
+  integrity sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw==
+  dependencies:
+    lodash "^4.17.5"
+
+which@^1.2.9:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+  dependencies:
+    isexe "^2.0.0"