Home Reference Source Repository

src/index.js

import path from 'path'
import * as commonTags from 'common-tags'

const defaultColors = [
  'bgBlue.bold',
  'bgMagenta.bold',
  'bgGreen.bold',
  'bgBlack.bold',
  'bgCyan.bold',
  'bgRed.bold',
  'bgWhite.bold',
  'bgYellow.bold',
  // TODO: add more colors that look good?
]

export {
  concurrent,
  series,
  runInNewWindow,
  rimraf,
  ifWindows,
  ifNotWindows,
  copy,
  mkdirp,
  open,
  crossEnv,
  commonTags,
}

/**
 * Accepts any number of scripts, filters out any
 * falsy ones and joins them with ' && '
 * @param {...string} scripts - Any number of strings representing commands
 * @example
 * // returns 'eslint && jest && webpack --env.production'
 * series('eslint', 'jest', 'webpack --env.production')
 * @return {string} - the command that will execute the given scripts in series
 */
function series(...scripts) {
  return scripts.filter(Boolean).join(' && ')
}

/**
 * Accepts any number of nps script names, filters out
 * any falsy ones, prepends `nps` to them, and passes
 * the that to `series`
 * @param {...string} scriptNames - the script names to run
 * // returns 'nps lint && nps "test --coverage" && nps build'
 * series.nps('lint', 'test --coverage', 'build')
 * @return {string} - the command that will execute the nps scripts in series
 */
series.nps = function seriesNPS(...scriptNames) {
  return series(
    ...scriptNames
      .filter(Boolean)
      .map(scriptName => scriptName.trim())
      .filter(Boolean)
      .map(scriptName => `nps ${quoteScript(scriptName)}`),
  )
}

/**
 * A concurrent script object
 * @typedef {Object|string} ConcurrentScript
 * @property {string} script - the command to run
 * @property {string} color - the color to use
 *   (see concurrently's docs for valid values)
 */
/**
 * An object of concurrent script objects
 * @typedef {Object.<ConcurrentScript>} ConcurrentScripts
 */

/**
 * Generates a command that uses `concurrently` to run
 * scripts concurrently. Adds a few flags to make it
 * behave as you probably want (like --kill-others-on-fail).
 * In addition, it adds color and labels where the color
 * can be specified or is defaulted and the label is based
 * on the key for the script.
 * @param {ConcurrentScripts} scripts - the scripts to run
 *   note: this function filters out falsy values :)
 * @example
 * // returns a bit of a long script that can vary slightly
 * // based on your environment... :)
 * concurrent({
 *   lint: {
 *     script: 'eslint .',
 *     color: 'bgGreen.white.dim',
 *   },
 *   test: 'jest',
 *   build: {
 *     script: 'webpack'
 *   }
 * })
 * @return {string} - the command to run
 */
function concurrent(scripts) {
  const {
    colors,
    scripts: quotedScripts,
    names,
  } = Object.keys(scripts).reduce(reduceScripts, {
    colors: [],
    scripts: [],
    names: [],
  })
  const flags = [
    '--kill-others-on-fail',
    `--prefix-colors "${colors.join(',')}"`,
    '--prefix "[{name}]"',
    `--names "${names.join(',')}"`,
    shellEscape(quotedScripts),
  ]
  const concurrently = getBin('concurrently')
  return `${concurrently} ${flags.join(' ')}`

  function reduceScripts(accumulator, scriptName, index) {
    let scriptObj = scripts[scriptName]
    if (!scriptObj) {
      return accumulator
    } else if (typeof scriptObj === 'string') {
      scriptObj = {script: scriptObj}
    }
    const {
      script,
      color = defaultColors[index % defaultColors.length],
    } = scriptObj
    if (!script) {
      return accumulator
    }
    accumulator.names.push(scriptName)
    accumulator.colors.push(color)
    accumulator.scripts.push(script)
    return accumulator
  }
}

/**
 * Accepts any number of nps script names, filters out
 * any falsy ones, prepends `nps` to them, and passes
 * the that to `concurrent`
 * @param {...string} scriptNames - the script names to run
 * @example
 * // will basically return `nps lint & nps "test --coverage" & nps build`
 * // but with the concurrently command and relevant flags to make
 * // it super awesome with colors and whatnot. :)
 * concurrent.nps('lint', 'test --coverage', 'build')
 * @return {string} the command to run
 */
concurrent.nps = function concurrentNPS(...scriptNames) {
  return concurrent(
    scriptNames.map(mapNPSScripts).reduce(reduceNPSScripts, {}),
  )

  function mapNPSScripts(scriptName, index) {
    const color = defaultColors[index]
    if (!Boolean(scriptName)) {
      return undefined
    } else if (typeof scriptName === 'string') {
      return {script: scriptName, color}
    } else {
      return Object.assign({color}, scriptName)
    }
  }

  function reduceNPSScripts(scripts, scriptObj) {
    if (!scriptObj) {
      return scripts
    }
    const {color, script} = scriptObj
    const [name] = script.split(' ')
    scripts[name] = {
      script: `nps ${quoteScript(script.trim())}`,
      color,
    }
    return scripts
  }
}

/**
 * EXPERIMENTAL: THIS DOES NOT CURRENTLY WORK FOR ALL TERMINALS
 * Takes a command and returns a version that should run in
 * another tab/window of your terminal. Currently only supports
 * Windows cmd (new window) and Terminal.app (new tab)
 * @param {string} command - the command to run in a new tab/window
 * @example
 * // returns some voodoo magic to make the terminal do what you want
 * runInNewWindow('echo hello')
 * @return {string} - the command to run
 */
function runInNewWindow(command) {
  return isWindows() ?
    `start cmd /k "cd ${process.cwd()} && ${command}"` :
    commonTags.oneLine`
      osascript
      -e 'tell application "Terminal"'
      -e 'tell application "System Events"
      to keystroke "t" using {command down}'
      -e 'do script "cd ${process.cwd()} && ${command}" in front window'
      -e 'end tell'
    `
}

/**
 * EXPERIMENTAL: THIS DOES NOT CURRENTLY WORK FOR ALL TERMINALS
 * Takes an nps script name and prepends it with a call to nps
 * then forwards that to `runInNewWindow` properly escaped.
 * @param {string} scriptName - the name of the nps script to run
 * @example
 * // returns a script that runs
 * // `node node_modules/.bin/nps "lint --cache"`
 * // in a new tab/window
 * runInNewWindow.nps('lint --cache')
 * @return {string} - the command to run
 */
runInNewWindow.nps = function runInNewWindowNPS(scriptName) {
  const escaped = true
  return runInNewWindow(
    `node node_modules/.bin/nps ${quoteScript(scriptName, escaped)}`,
  )
}

/**
 * Gets a script that uses the rimraf binary. rimraf
 * is a dependency of nps-utils, so you don't need to
 * install it yourself.
 * @param {string} args - args to pass to rimraf
 *   learn more from http://npm.im/rimraf
 * @return {string} - the command with the rimraf binary
 */
function rimraf(args) {
  return `${getBin('rimraf')} ${args}`
}

/**
 * Takes two scripts and returns the first if the
 * current environment is windows, and the second
 * if the current environment is not windows
 * @param {string} script - the script to use for windows
 * @param {string} altScript - the script to use for non-windows
 * @return {string} - the command to run
 */
function ifWindows(script, altScript) {
  return isWindows() ? script : altScript
}

/**
 * Simply calls ifWindows(altScript, script)
 * @param {string} script - the script to use for non-windows
 * @param {string} altScript - the script to use for windows
 * @return {string} - the command to run
 */
function ifNotWindows(script, altScript) {
  return ifWindows(altScript, script)
}

/**
 * Gets a script that uses the cpy-cli binary. cpy-cli
 * is a dependency of nps-utils, so you don't need to
 * install it yourself.
 * @param {string} args - args to pass to cpy-cli
 *   learn more from http://npm.im/cpy-cli
 * @return {string} - the command with the cpy-cli binary
 */
function copy(args) {
  return `${getBin('cpy-cli', 'cpy')} ${args}`
}

/**
 * Gets a script that uses the mkdirp binary. mkdirp
 * is a dependency of nps-utils, so you don't need to
 * install it yourself.
 * @param {string} args - args to pass to mkdirp
 *   learn more from http://npm.im/mkdirp
 * @return {string} - the command with the mkdirp binary
 */
function mkdirp(args) {
  return `${getBin('mkdirp')} ${args}`
}

/**
 * Gets a script that uses the opn-cli binary. opn-cli
 * is a dependency of nps-utils, so you don't need to
 * install it yourself.
 * @param {string} args - args to pass to opn-cli
 *   learn more from http://npm.im/opn-cli
 * @return {string} - the command with the opn-cli binary
 */
function open(args) {
  return `${getBin('opn-cli', 'opn')} ${args}`
}

/**
 * Gets a script that uses the cross-env binary. cross-env
 * is a dependency of nps-utils, so you don't need to
 * install it yourself.
 * @param {string} args - args to pass to cross-env
 *   learn more from http://npm.im/cross-env
 * @return {string} - the command with the cross-env binary
 */
function crossEnv(args) {
  return `${getBin('cross-env')} ${args}`
}

// utils

function quoteScript(script, escaped) {
  const quote = escaped ? '\\"' : '"'
  const shouldQuote = script.indexOf(' ') !== -1
  return shouldQuote ? `${quote}${script}${quote}` : script
}

function getBin(packageName, binName = packageName) {
  const packagePath = require.resolve(`${packageName}/package.json`)
  const concurrentlyDir = path.dirname(packagePath)
  let {bin: binRelativeToPackage} = require(packagePath)
  if (typeof binRelativeToPackage === 'object') {
    binRelativeToPackage = binRelativeToPackage[binName]
  }
  const fullBinPath = path.join(concurrentlyDir, binRelativeToPackage)
  const relativeBinPath = path.relative(process.cwd(), fullBinPath)
  return `node ${relativeBinPath}`
}

function isWindows() {
  // lazily require for perf :)
  return require('is-windows')()
}

function shellEscape(...args) {
  // lazily require for perf :)
  return require('any-shell-escape')(...args)
}

/*
  eslint
    func-name-matching:0,
    global-require:0,
    import/no-dynamic-require:0
*/