index.js (6516B)
1 'use strict'; 2 const path = require('path'); 3 const childProcess = require('child_process'); 4 const crossSpawn = require('cross-spawn'); 5 const stripFinalNewline = require('strip-final-newline'); 6 const npmRunPath = require('npm-run-path'); 7 const onetime = require('onetime'); 8 const makeError = require('./lib/error'); 9 const normalizeStdio = require('./lib/stdio'); 10 const {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} = require('./lib/kill'); 11 const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = require('./lib/stream'); 12 const {mergePromise, getSpawnedPromise} = require('./lib/promise'); 13 const {joinCommand, parseCommand, getEscapedCommand} = require('./lib/command'); 14 15 const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; 16 17 const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { 18 const env = extendEnv ? {...process.env, ...envOption} : envOption; 19 20 if (preferLocal) { 21 return npmRunPath.env({env, cwd: localDir, execPath}); 22 } 23 24 return env; 25 }; 26 27 const handleArguments = (file, args, options = {}) => { 28 const parsed = crossSpawn._parse(file, args, options); 29 file = parsed.command; 30 args = parsed.args; 31 options = parsed.options; 32 33 options = { 34 maxBuffer: DEFAULT_MAX_BUFFER, 35 buffer: true, 36 stripFinalNewline: true, 37 extendEnv: true, 38 preferLocal: false, 39 localDir: options.cwd || process.cwd(), 40 execPath: process.execPath, 41 encoding: 'utf8', 42 reject: true, 43 cleanup: true, 44 all: false, 45 windowsHide: true, 46 ...options 47 }; 48 49 options.env = getEnv(options); 50 51 options.stdio = normalizeStdio(options); 52 53 if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { 54 // #116 55 args.unshift('/q'); 56 } 57 58 return {file, args, options, parsed}; 59 }; 60 61 const handleOutput = (options, value, error) => { 62 if (typeof value !== 'string' && !Buffer.isBuffer(value)) { 63 // When `execa.sync()` errors, we normalize it to '' to mimic `execa()` 64 return error === undefined ? undefined : ''; 65 } 66 67 if (options.stripFinalNewline) { 68 return stripFinalNewline(value); 69 } 70 71 return value; 72 }; 73 74 const execa = (file, args, options) => { 75 const parsed = handleArguments(file, args, options); 76 const command = joinCommand(file, args); 77 const escapedCommand = getEscapedCommand(file, args); 78 79 validateTimeout(parsed.options); 80 81 let spawned; 82 try { 83 spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); 84 } catch (error) { 85 // Ensure the returned error is always both a promise and a child process 86 const dummySpawned = new childProcess.ChildProcess(); 87 const errorPromise = Promise.reject(makeError({ 88 error, 89 stdout: '', 90 stderr: '', 91 all: '', 92 command, 93 escapedCommand, 94 parsed, 95 timedOut: false, 96 isCanceled: false, 97 killed: false 98 })); 99 return mergePromise(dummySpawned, errorPromise); 100 } 101 102 const spawnedPromise = getSpawnedPromise(spawned); 103 const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); 104 const processDone = setExitHandler(spawned, parsed.options, timedPromise); 105 106 const context = {isCanceled: false}; 107 108 spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); 109 spawned.cancel = spawnedCancel.bind(null, spawned, context); 110 111 const handlePromise = async () => { 112 const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); 113 const stdout = handleOutput(parsed.options, stdoutResult); 114 const stderr = handleOutput(parsed.options, stderrResult); 115 const all = handleOutput(parsed.options, allResult); 116 117 if (error || exitCode !== 0 || signal !== null) { 118 const returnedError = makeError({ 119 error, 120 exitCode, 121 signal, 122 stdout, 123 stderr, 124 all, 125 command, 126 escapedCommand, 127 parsed, 128 timedOut, 129 isCanceled: context.isCanceled, 130 killed: spawned.killed 131 }); 132 133 if (!parsed.options.reject) { 134 return returnedError; 135 } 136 137 throw returnedError; 138 } 139 140 return { 141 command, 142 escapedCommand, 143 exitCode: 0, 144 stdout, 145 stderr, 146 all, 147 failed: false, 148 timedOut: false, 149 isCanceled: false, 150 killed: false 151 }; 152 }; 153 154 const handlePromiseOnce = onetime(handlePromise); 155 156 handleInput(spawned, parsed.options.input); 157 158 spawned.all = makeAllStream(spawned, parsed.options); 159 160 return mergePromise(spawned, handlePromiseOnce); 161 }; 162 163 module.exports = execa; 164 165 module.exports.sync = (file, args, options) => { 166 const parsed = handleArguments(file, args, options); 167 const command = joinCommand(file, args); 168 const escapedCommand = getEscapedCommand(file, args); 169 170 validateInputSync(parsed.options); 171 172 let result; 173 try { 174 result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options); 175 } catch (error) { 176 throw makeError({ 177 error, 178 stdout: '', 179 stderr: '', 180 all: '', 181 command, 182 escapedCommand, 183 parsed, 184 timedOut: false, 185 isCanceled: false, 186 killed: false 187 }); 188 } 189 190 const stdout = handleOutput(parsed.options, result.stdout, result.error); 191 const stderr = handleOutput(parsed.options, result.stderr, result.error); 192 193 if (result.error || result.status !== 0 || result.signal !== null) { 194 const error = makeError({ 195 stdout, 196 stderr, 197 error: result.error, 198 signal: result.signal, 199 exitCode: result.status, 200 command, 201 escapedCommand, 202 parsed, 203 timedOut: result.error && result.error.code === 'ETIMEDOUT', 204 isCanceled: false, 205 killed: result.signal !== null 206 }); 207 208 if (!parsed.options.reject) { 209 return error; 210 } 211 212 throw error; 213 } 214 215 return { 216 command, 217 escapedCommand, 218 exitCode: 0, 219 stdout, 220 stderr, 221 failed: false, 222 timedOut: false, 223 isCanceled: false, 224 killed: false 225 }; 226 }; 227 228 module.exports.command = (command, options) => { 229 const [file, ...args] = parseCommand(command); 230 return execa(file, args, options); 231 }; 232 233 module.exports.commandSync = (command, options) => { 234 const [file, ...args] = parseCommand(command); 235 return execa.sync(file, args, options); 236 }; 237 238 module.exports.node = (scriptPath, args, options = {}) => { 239 if (args && !Array.isArray(args) && typeof args === 'object') { 240 options = args; 241 args = []; 242 } 243 244 const stdio = normalizeStdio.node(options); 245 const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); 246 247 const { 248 nodePath = process.execPath, 249 nodeOptions = defaultExecArgv 250 } = options; 251 252 return execa( 253 nodePath, 254 [ 255 ...nodeOptions, 256 scriptPath, 257 ...(Array.isArray(args) ? args : []) 258 ], 259 { 260 ...options, 261 stdin: undefined, 262 stdout: undefined, 263 stderr: undefined, 264 stdio, 265 shell: false 266 } 267 ); 268 };