index.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. /**
  2. * @module index
  3. * @author Toru Nagashima
  4. * @copyright 2015 Toru Nagashima. All rights reserved.
  5. * See LICENSE file in root directory for full license.
  6. */
  7. 'use strict'
  8. // ------------------------------------------------------------------------------
  9. // Requirements
  10. // ------------------------------------------------------------------------------
  11. const shellQuote = require('shell-quote')
  12. const matchTasks = require('./match-tasks')
  13. const readPackageJson = require('./read-package-json')
  14. const runTasks = require('./run-tasks')
  15. // ------------------------------------------------------------------------------
  16. // Helpers
  17. // ------------------------------------------------------------------------------
  18. const ARGS_PATTERN = /\{(!)?([*@%]|\d+)([^}]+)?}/g
  19. const ARGS_UNPACK_PATTERN = /\{(!)?([%])([^}]+)?}/g
  20. /**
  21. * Converts a given value to an array.
  22. *
  23. * @param {string|string[]|null|undefined} x - A value to convert.
  24. * @returns {string[]} An array.
  25. */
  26. function toArray (x) {
  27. if (x == null) {
  28. return []
  29. }
  30. return Array.isArray(x) ? x : [x]
  31. }
  32. /**
  33. * Replaces argument placeholders (such as `{1}`) by arguments.
  34. *
  35. * @param {string[]} patterns - Patterns to replace.
  36. * @param {string[]} args - Arguments to replace.
  37. * @returns {string[]} replaced
  38. */
  39. function applyArguments (patterns, args) {
  40. const defaults = Object.create(null)
  41. const unfoldedPatterns = patterns
  42. .flatMap(pattern => {
  43. const match = ARGS_UNPACK_PATTERN.exec(pattern)
  44. if (match && match[2] === '%') {
  45. const result = []
  46. for (let i = 0, length = args.length; i < length; i++) {
  47. const argPosition = i + 1
  48. result.push(pattern.replace(ARGS_UNPACK_PATTERN, (whole, indirectionMark, id, options) => {
  49. if (indirectionMark != null || options != null || id !== '%') {
  50. throw Error(`Invalid Placeholder: ${whole}`)
  51. }
  52. return `{${argPosition}}`
  53. }))
  54. }
  55. return result
  56. }
  57. return pattern
  58. })
  59. return unfoldedPatterns.map(pattern => pattern.replace(ARGS_PATTERN, (whole, indirectionMark, id, options) => {
  60. if (indirectionMark != null) {
  61. throw Error(`Invalid Placeholder: ${whole}`)
  62. }
  63. if (id === '@') {
  64. return shellQuote.quote(args)
  65. }
  66. if (id === '*') {
  67. return shellQuote.quote([args.join(' ')])
  68. }
  69. const position = parseInt(id, 10)
  70. if (position >= 1 && position <= args.length) {
  71. return shellQuote.quote([args[position - 1]])
  72. }
  73. // Address default values
  74. if (options != null) {
  75. const prefix = options.slice(0, 2)
  76. if (prefix === ':=') {
  77. defaults[id] = shellQuote.quote([options.slice(2)])
  78. return defaults[id]
  79. }
  80. if (prefix === ':-') {
  81. return shellQuote.quote([options.slice(2)])
  82. }
  83. throw Error(`Invalid Placeholder: ${whole}`)
  84. }
  85. if (defaults[id] != null) {
  86. return defaults[id]
  87. }
  88. return ''
  89. }))
  90. }
  91. /**
  92. * Parse patterns.
  93. * In parsing process, it replaces argument placeholders (such as `{1}`) by arguments.
  94. *
  95. * @param {string|string[]} patternOrPatterns - Patterns to run.
  96. * A pattern is a npm-script name or a Glob-like pattern.
  97. * @param {string[]} args - Arguments to replace placeholders.
  98. * @returns {string[]} Parsed patterns.
  99. */
  100. function parsePatterns (patternOrPatterns, args) {
  101. const patterns = toArray(patternOrPatterns)
  102. const hasPlaceholder = patterns.some(pattern => ARGS_PATTERN.test(pattern))
  103. return hasPlaceholder ? applyArguments(patterns, args) : patterns
  104. }
  105. /**
  106. * Converts a given config object to an `--:=` style option array.
  107. *
  108. * @param {object|null} config -
  109. * A map-like object to overwrite package configs.
  110. * Keys are package names.
  111. * Every value is a map-like object (Pairs of variable name and value).
  112. * @returns {string[]} `--:=` style options.
  113. */
  114. function toOverwriteOptions (config) {
  115. const options = []
  116. for (const packageName of Object.keys(config)) {
  117. const packageConfig = config[packageName]
  118. for (const variableName of Object.keys(packageConfig)) {
  119. const value = packageConfig[variableName]
  120. options.push(`--${packageName}:${variableName}=${value}`)
  121. }
  122. }
  123. return options
  124. }
  125. /**
  126. * Converts a given config object to an `--a=b` style option array.
  127. *
  128. * @param {object|null} config -
  129. * A map-like object to set configs.
  130. * @returns {string[]} `--a=b` style options.
  131. */
  132. function toConfigOptions (config) {
  133. return Object.keys(config).map(key => `--${key}=${config[key]}`)
  134. }
  135. /**
  136. * Gets the maximum length.
  137. *
  138. * @param {number} length - The current maximum length.
  139. * @param {string} name - A name.
  140. * @returns {number} The maximum length.
  141. */
  142. function maxLength (length, name) {
  143. return Math.max(name.length, length)
  144. }
  145. // ------------------------------------------------------------------------------
  146. // Public Interface
  147. // ------------------------------------------------------------------------------
  148. /**
  149. * Runs npm-scripts which are matched with given patterns.
  150. *
  151. * @param {string|string[]} patternOrPatterns - Patterns to run.
  152. * A pattern is a npm-script name or a Glob-like pattern.
  153. * @param {object|undefined} [options] Optional.
  154. * @param {boolean} options.parallel -
  155. * If this is `true`, run scripts in parallel.
  156. * Otherwise, run scripts in sequencial.
  157. * Default is `false`.
  158. * @param {stream.Readable|null} options.stdin -
  159. * A readable stream to send messages to stdin of child process.
  160. * If this is `null`, ignores it.
  161. * If this is `process.stdin`, inherits it.
  162. * Otherwise, makes a pipe.
  163. * Default is `null`.
  164. * @param {stream.Writable|null} options.stdout -
  165. * A writable stream to receive messages from stdout of child process.
  166. * If this is `null`, cannot send.
  167. * If this is `process.stdout`, inherits it.
  168. * Otherwise, makes a pipe.
  169. * Default is `null`.
  170. * @param {stream.Writable|null} options.stderr -
  171. * A writable stream to receive messages from stderr of child process.
  172. * If this is `null`, cannot send.
  173. * If this is `process.stderr`, inherits it.
  174. * Otherwise, makes a pipe.
  175. * Default is `null`.
  176. * @param {string[]} options.taskList -
  177. * Actual name list of npm-scripts.
  178. * This function search npm-script names in this list.
  179. * If this is `null`, this function reads `package.json` of current directly.
  180. * @param {object|null} options.packageConfig -
  181. * A map-like object to overwrite package configs.
  182. * Keys are package names.
  183. * Every value is a map-like object (Pairs of variable name and value).
  184. * e.g. `{"npm-run-all": {"test": 777}}`
  185. * Default is `null`.
  186. * @param {boolean} options.silent -
  187. * The flag to set `silent` to the log level of npm.
  188. * Default is `false`.
  189. * @param {boolean} options.continueOnError -
  190. * The flag to ignore errors.
  191. * Default is `false`.
  192. * @param {boolean} options.printLabel -
  193. * The flag to print task names at the head of each line.
  194. * Default is `false`.
  195. * @param {boolean} options.printName -
  196. * The flag to print task names before running each task.
  197. * Default is `false`.
  198. * @param {number} options.maxParallel -
  199. * The maximum number of parallelism.
  200. * Default is unlimited.
  201. * @param {string} options.npmPath -
  202. * The path to npm.
  203. * Default is `process.env.npm_execpath`.
  204. * @returns {Promise}
  205. * A promise object which becomes fullfilled when all npm-scripts are completed.
  206. */
  207. module.exports = function npmRunAll (patternOrPatterns, options) {
  208. const stdin = (options && options.stdin) || null
  209. const stdout = (options && options.stdout) || null
  210. const stderr = (options && options.stderr) || null
  211. const taskList = (options && options.taskList) || null
  212. const config = (options && options.config) || null
  213. const packageConfig = (options && options.packageConfig) || null
  214. const args = (options && options.arguments) || []
  215. const parallel = Boolean(options && options.parallel)
  216. const silent = Boolean(options && options.silent)
  217. const continueOnError = Boolean(options && options.continueOnError)
  218. const printLabel = Boolean(options && options.printLabel)
  219. const printName = Boolean(options && options.printName)
  220. const race = Boolean(options && options.race)
  221. const maxParallel = parallel ? ((options && options.maxParallel) || 0) : 1
  222. const aggregateOutput = Boolean(options && options.aggregateOutput)
  223. const npmPath = options && options.npmPath
  224. try {
  225. const patterns = parsePatterns(patternOrPatterns, args)
  226. if (patterns.length === 0) {
  227. return Promise.resolve(null)
  228. }
  229. if (taskList != null && Array.isArray(taskList) === false) {
  230. throw new Error('Invalid options.taskList')
  231. }
  232. if (typeof maxParallel !== 'number' || !(maxParallel >= 0)) {
  233. throw new Error('Invalid options.maxParallel')
  234. }
  235. if (!parallel && aggregateOutput) {
  236. throw new Error('Invalid options.aggregateOutput; It requires options.parallel')
  237. }
  238. if (!parallel && race) {
  239. throw new Error('Invalid options.race; It requires options.parallel')
  240. }
  241. const prefixOptions = [].concat(
  242. silent ? ['--silent'] : [],
  243. packageConfig ? toOverwriteOptions(packageConfig) : [],
  244. config ? toConfigOptions(config) : []
  245. )
  246. return Promise.resolve()
  247. .then(() => {
  248. if (taskList != null) {
  249. return { taskList, packageInfo: null }
  250. }
  251. return readPackageJson()
  252. })
  253. .then(x => {
  254. const tasks = matchTasks(x.taskList, patterns)
  255. const labelWidth = tasks.reduce(maxLength, 0)
  256. return runTasks(tasks, {
  257. stdin,
  258. stdout,
  259. stderr,
  260. prefixOptions,
  261. continueOnError,
  262. labelState: {
  263. enabled: printLabel,
  264. width: labelWidth,
  265. lastPrefix: null,
  266. lastIsLinebreak: true,
  267. },
  268. printName,
  269. packageInfo: x.packageInfo,
  270. race,
  271. maxParallel,
  272. npmPath,
  273. aggregateOutput,
  274. })
  275. })
  276. } catch (err) {
  277. return Promise.reject(new Error(err.message))
  278. }
  279. }