UNPKG

6.6 kBJavaScriptView Raw
1/**
2 * @module run-task
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 path = require("path")
14const chalk = require("chalk")
15const parseArgs = require("shell-quote").parse
16const padEnd = require("string.prototype.padend")
17const createHeader = require("./create-header")
18const createPrefixTransform = require("./create-prefix-transform-stream")
19const spawn = require("./spawn")
20
21//------------------------------------------------------------------------------
22// Helpers
23//------------------------------------------------------------------------------
24
25const colors = [chalk.cyan, chalk.green, chalk.magenta, chalk.yellow, chalk.red]
26
27/**
28 * Select a color from given task name.
29 *
30 * @param {string} taskName - The task name.
31 * @returns {function} A colorize function that provided by `chalk`
32 */
33function selectColor(taskName) {
34 let hash = 0
35
36 for (const c of taskName) {
37 hash = ((hash << 5) - hash) + c
38 hash |= 0
39 }
40
41 return colors[Math.abs(hash) % colors.length]
42}
43
44/**
45 * Wraps stdout/stderr with a transform stream to add the task name as prefix.
46 *
47 * @param {string} taskName - The task name.
48 * @param {stream.Writable} source - An output stream to be wrapped.
49 * @param {object} labelState - An label state for the transform stream.
50 * @returns {stream.Writable} `source` or the created wrapped stream.
51 */
52function wrapLabeling(taskName, source, labelState) {
53 if (source == null || !labelState.enabled) {
54 return source
55 }
56
57 const label = padEnd(taskName, labelState.width)
58 const color = source.isTTY ? selectColor(taskName) : (x) => x
59 const prefix = color(`[${label}] `)
60 const stream = createPrefixTransform(prefix, labelState)
61
62 stream.pipe(source)
63
64 return stream
65}
66
67/**
68 * Converts a given stream to an option for `child_process.spawn`.
69 *
70 * @param {stream.Readable|stream.Writable|null} stream - An original stream to convert.
71 * @param {process.stdin|process.stdout|process.stderr} std - A standard stream for this option.
72 * @returns {string|stream.Readable|stream.Writable} An option for `child_process.spawn`.
73 */
74function detectStreamKind(stream, std) {
75 return (
76 stream == null ? "ignore" :
77 // `|| !std.isTTY` is needed for the workaround of https://github.com/nodejs/node/issues/5620
78 stream !== std || !std.isTTY ? "pipe" :
79 /* else */ stream
80 )
81}
82
83//------------------------------------------------------------------------------
84// Interface
85//------------------------------------------------------------------------------
86
87/**
88 * Run a npm-script of a given name.
89 * The return value is a promise which has an extra method: `abort()`.
90 * The `abort()` kills the child process to run the npm-script.
91 *
92 * @param {string} task - A npm-script name to run.
93 * @param {object} options - An option object.
94 * @param {stream.Readable|null} options.stdin -
95 * A readable stream to send messages to stdin of child process.
96 * If this is `null`, ignores it.
97 * If this is `process.stdin`, inherits it.
98 * Otherwise, makes a pipe.
99 * @param {stream.Writable|null} options.stdout -
100 * A writable stream to receive messages from stdout of child process.
101 * If this is `null`, cannot send.
102 * If this is `process.stdout`, inherits it.
103 * Otherwise, makes a pipe.
104 * @param {stream.Writable|null} options.stderr -
105 * A writable stream to receive messages from stderr of child process.
106 * If this is `null`, cannot send.
107 * If this is `process.stderr`, inherits it.
108 * Otherwise, makes a pipe.
109 * @param {string[]} options.prefixOptions -
110 * An array of options which are inserted before the task name.
111 * @param {object} options.labelState - A state object for printing labels.
112 * @param {boolean} options.printName - The flag to print task names before running each task.
113 * @returns {Promise}
114 * A promise object which becomes fullfilled when the npm-script is completed.
115 * This promise object has an extra method: `abort()`.
116 * @private
117 */
118module.exports = function runTask(task, options) {
119 let cp = null
120 const promise = new Promise((resolve, reject) => {
121 const stdin = options.stdin
122 const stdout = wrapLabeling(task, options.stdout, options.labelState)
123 const stderr = wrapLabeling(task, options.stderr, options.labelState)
124 const stdinKind = detectStreamKind(stdin, process.stdin)
125 const stdoutKind = detectStreamKind(stdout, process.stdout)
126 const stderrKind = detectStreamKind(stderr, process.stderr)
127 const spawnOptions = { stdio: [stdinKind, stdoutKind, stderrKind] }
128
129 // Print task name.
130 if (options.printName && stdout != null) {
131 stdout.write(createHeader(
132 task,
133 options.packageInfo,
134 options.stdout.isTTY
135 ))
136 }
137
138 if (path.extname(options.npmPath || "a.js") === ".js") {
139 const npmPath = options.npmPath || process.env.npm_execpath //eslint-disable-line no-process-env
140 const execPath = npmPath ? process.execPath : "npm"
141 const spawnArgs = [].concat(
142 npmPath ? [npmPath, "run"] : ["run"],
143 options.prefixOptions,
144 parseArgs(task)
145 )
146
147 // Execute.
148 cp = spawn(execPath, spawnArgs, spawnOptions)
149 }
150 else {
151 const execPath = options.npmPath
152 const spawnArgs = [].concat(
153 ["run"],
154 options.prefixOptions,
155 parseArgs(task)
156 )
157
158 // Execute.
159 cp = spawn(execPath, spawnArgs, spawnOptions)
160 }
161
162 // Piping stdio.
163 if (stdinKind === "pipe") {
164 stdin.pipe(cp.stdin)
165 }
166 if (stdoutKind === "pipe") {
167 cp.stdout.pipe(stdout, { end: false })
168 }
169 if (stderrKind === "pipe") {
170 cp.stderr.pipe(stderr, { end: false })
171 }
172
173 // Register
174 cp.on("error", (err) => {
175 cp = null
176 reject(err)
177 })
178 cp.on("close", (code) => {
179 cp = null
180 resolve({ task, code })
181 })
182 })
183
184 promise.abort = function abort() {
185 if (cp != null) {
186 cp.kill()
187 cp = null
188 }
189 }
190
191 return promise
192}