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