UNPKG

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