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 |
|
8 |
|
9 | //------------------------------------------------------------------------------
|
10 | // Requirements
|
11 | //------------------------------------------------------------------------------
|
12 |
|
13 | const path = require("path")
|
14 | const chalk = require("chalk")
|
15 | const parseArgs = require("shell-quote").parse
|
16 | const padEnd = require("string.prototype.padend")
|
17 | const createHeader = require("./create-header")
|
18 | const createPrefixTransform = require("./create-prefix-transform-stream")
|
19 | const spawn = require("./spawn")
|
20 |
|
21 | //------------------------------------------------------------------------------
|
22 | // Helpers
|
23 | //------------------------------------------------------------------------------
|
24 |
|
25 | const 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 | */
|
33 | function 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 | */
|
52 | function 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 | */
|
74 | function 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 | */
|
118 | module.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 | }
|