index.js
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, setExitHandler} = require('./lib/kill'); 11 const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = require('./lib/stream.js'); 12 const {mergePromise, getSpawnedPromise} = require('./lib/promise.js'); 13 const {joinCommand, parseCommand} = require('./lib/command.js'); 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 78 let spawned; 79 try { 80 spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); 81 } catch (error) { 82 // Ensure the returned error is always both a promise and a child process 83 const dummySpawned = new childProcess.ChildProcess(); 84 const errorPromise = Promise.reject(makeError({ 85 error, 86 stdout: '', 87 stderr: '', 88 all: '', 89 command, 90 parsed, 91 timedOut: false, 92 isCanceled: false, 93 killed: false 94 })); 95 return mergePromise(dummySpawned, errorPromise); 96 } 97 98 const spawnedPromise = getSpawnedPromise(spawned); 99 const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); 100 const processDone = setExitHandler(spawned, parsed.options, timedPromise); 101 102 const context = {isCanceled: false}; 103 104 spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); 105 spawned.cancel = spawnedCancel.bind(null, spawned, context); 106 107 const handlePromise = async () => { 108 const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); 109 const stdout = handleOutput(parsed.options, stdoutResult); 110 const stderr = handleOutput(parsed.options, stderrResult); 111 const all = handleOutput(parsed.options, allResult); 112 113 if (error || exitCode !== 0 || signal !== null) { 114 const returnedError = makeError({ 115 error, 116 exitCode, 117 signal, 118 stdout, 119 stderr, 120 all, 121 command, 122 parsed, 123 timedOut, 124 isCanceled: context.isCanceled, 125 killed: spawned.killed 126 }); 127 128 if (!parsed.options.reject) { 129 return returnedError; 130 } 131 132 throw returnedError; 133 } 134 135 return { 136 command, 137 exitCode: 0, 138 stdout, 139 stderr, 140 all, 141 failed: false, 142 timedOut: false, 143 isCanceled: false, 144 killed: false 145 }; 146 }; 147 148 const handlePromiseOnce = onetime(handlePromise); 149 150 crossSpawn._enoent.hookChildProcess(spawned, parsed.parsed); 151 152 handleInput(spawned, parsed.options.input); 153 154 spawned.all = makeAllStream(spawned, parsed.options); 155 156 return mergePromise(spawned, handlePromiseOnce); 157 }; 158 159 module.exports = execa; 160 161 module.exports.sync = (file, args, options) => { 162 const parsed = handleArguments(file, args, options); 163 const command = joinCommand(file, args); 164 165 validateInputSync(parsed.options); 166 167 let result; 168 try { 169 result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options); 170 } catch (error) { 171 throw makeError({ 172 error, 173 stdout: '', 174 stderr: '', 175 all: '', 176 command, 177 parsed, 178 timedOut: false, 179 isCanceled: false, 180 killed: false 181 }); 182 } 183 184 const stdout = handleOutput(parsed.options, result.stdout, result.error); 185 const stderr = handleOutput(parsed.options, result.stderr, result.error); 186 187 if (result.error || result.status !== 0 || result.signal !== null) { 188 const error = makeError({ 189 stdout, 190 stderr, 191 error: result.error, 192 signal: result.signal, 193 exitCode: result.status, 194 command, 195 parsed, 196 timedOut: result.error && result.error.code === 'ETIMEDOUT', 197 isCanceled: false, 198 killed: result.signal !== null 199 }); 200 201 if (!parsed.options.reject) { 202 return error; 203 } 204 205 throw error; 206 } 207 208 return { 209 command, 210 exitCode: 0, 211 stdout, 212 stderr, 213 failed: false, 214 timedOut: false, 215 isCanceled: false, 216 killed: false 217 }; 218 }; 219 220 module.exports.command = (command, options) => { 221 const [file, ...args] = parseCommand(command); 222 return execa(file, args, options); 223 }; 224 225 module.exports.commandSync = (command, options) => { 226 const [file, ...args] = parseCommand(command); 227 return execa.sync(file, args, options); 228 }; 229 230 module.exports.node = (scriptPath, args, options = {}) => { 231 if (args && !Array.isArray(args) && typeof args === 'object') { 232 options = args; 233 args = []; 234 } 235 236 const stdio = normalizeStdio.node(options); 237 const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); 238 239 const { 240 nodePath = process.execPath, 241 nodeOptions = defaultExecArgv 242 } = options; 243 244 return execa( 245 nodePath, 246 [ 247 ...nodeOptions, 248 scriptPath, 249 ...(Array.isArray(args) ? args : []) 250 ], 251 { 252 ...options, 253 stdin: undefined, 254 stdout: undefined, 255 stderr: undefined, 256 stdio, 257 shell: false 258 } 259 ); 260 };