|
import {Buffer} from 'node:buffer'; |
|
import path from 'node:path'; |
|
import childProcess from 'node:child_process'; |
|
import process from 'node:process'; |
|
import crossSpawn from 'cross-spawn'; |
|
import stripFinalNewline from 'strip-final-newline'; |
|
import {npmRunPathEnv} from 'npm-run-path'; |
|
import onetime from 'onetime'; |
|
import {makeError} from './lib/error.js'; |
|
import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; |
|
import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; |
|
import {addPipeMethods} from './lib/pipe.js'; |
|
import {handleInput, getSpawnedResult, makeAllStream, handleInputSync} from './lib/stream.js'; |
|
import {mergePromise, getSpawnedPromise} from './lib/promise.js'; |
|
import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js'; |
|
import {logCommand, verboseDefault} from './lib/verbose.js'; |
|
|
|
const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; |
|
|
|
const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { |
|
const env = extendEnv ? {...process.env, ...envOption} : envOption; |
|
|
|
if (preferLocal) { |
|
return npmRunPathEnv({env, cwd: localDir, execPath}); |
|
} |
|
|
|
return env; |
|
}; |
|
|
|
const handleArguments = (file, args, options = {}) => { |
|
const parsed = crossSpawn._parse(file, args, options); |
|
file = parsed.command; |
|
args = parsed.args; |
|
options = parsed.options; |
|
|
|
options = { |
|
maxBuffer: DEFAULT_MAX_BUFFER, |
|
buffer: true, |
|
stripFinalNewline: true, |
|
extendEnv: true, |
|
preferLocal: false, |
|
localDir: options.cwd || process.cwd(), |
|
execPath: process.execPath, |
|
encoding: 'utf8', |
|
reject: true, |
|
cleanup: true, |
|
all: false, |
|
windowsHide: true, |
|
verbose: verboseDefault, |
|
...options, |
|
}; |
|
|
|
options.env = getEnv(options); |
|
|
|
options.stdio = normalizeStdio(options); |
|
|
|
if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { |
|
|
|
args.unshift('/q'); |
|
} |
|
|
|
return {file, args, options, parsed}; |
|
}; |
|
|
|
const handleOutput = (options, value, error) => { |
|
if (typeof value !== 'string' && !Buffer.isBuffer(value)) { |
|
|
|
return error === undefined ? undefined : ''; |
|
} |
|
|
|
if (options.stripFinalNewline) { |
|
return stripFinalNewline(value); |
|
} |
|
|
|
return value; |
|
}; |
|
|
|
export function execa(file, args, options) { |
|
const parsed = handleArguments(file, args, options); |
|
const command = joinCommand(file, args); |
|
const escapedCommand = getEscapedCommand(file, args); |
|
logCommand(escapedCommand, parsed.options); |
|
|
|
validateTimeout(parsed.options); |
|
|
|
let spawned; |
|
try { |
|
spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); |
|
} catch (error) { |
|
|
|
const dummySpawned = new childProcess.ChildProcess(); |
|
const errorPromise = Promise.reject(makeError({ |
|
error, |
|
stdout: '', |
|
stderr: '', |
|
all: '', |
|
command, |
|
escapedCommand, |
|
parsed, |
|
timedOut: false, |
|
isCanceled: false, |
|
killed: false, |
|
})); |
|
mergePromise(dummySpawned, errorPromise); |
|
return dummySpawned; |
|
} |
|
|
|
const spawnedPromise = getSpawnedPromise(spawned); |
|
const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); |
|
const processDone = setExitHandler(spawned, parsed.options, timedPromise); |
|
|
|
const context = {isCanceled: false}; |
|
|
|
spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); |
|
spawned.cancel = spawnedCancel.bind(null, spawned, context); |
|
|
|
const handlePromise = async () => { |
|
const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); |
|
const stdout = handleOutput(parsed.options, stdoutResult); |
|
const stderr = handleOutput(parsed.options, stderrResult); |
|
const all = handleOutput(parsed.options, allResult); |
|
|
|
if (error || exitCode !== 0 || signal !== null) { |
|
const returnedError = makeError({ |
|
error, |
|
exitCode, |
|
signal, |
|
stdout, |
|
stderr, |
|
all, |
|
command, |
|
escapedCommand, |
|
parsed, |
|
timedOut, |
|
isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false), |
|
killed: spawned.killed, |
|
}); |
|
|
|
if (!parsed.options.reject) { |
|
return returnedError; |
|
} |
|
|
|
throw returnedError; |
|
} |
|
|
|
return { |
|
command, |
|
escapedCommand, |
|
exitCode: 0, |
|
stdout, |
|
stderr, |
|
all, |
|
failed: false, |
|
timedOut: false, |
|
isCanceled: false, |
|
killed: false, |
|
}; |
|
}; |
|
|
|
const handlePromiseOnce = onetime(handlePromise); |
|
|
|
handleInput(spawned, parsed.options); |
|
|
|
spawned.all = makeAllStream(spawned, parsed.options); |
|
|
|
addPipeMethods(spawned); |
|
mergePromise(spawned, handlePromiseOnce); |
|
return spawned; |
|
} |
|
|
|
export function execaSync(file, args, options) { |
|
const parsed = handleArguments(file, args, options); |
|
const command = joinCommand(file, args); |
|
const escapedCommand = getEscapedCommand(file, args); |
|
logCommand(escapedCommand, parsed.options); |
|
|
|
const input = handleInputSync(parsed.options); |
|
|
|
let result; |
|
try { |
|
result = childProcess.spawnSync(parsed.file, parsed.args, {...parsed.options, input}); |
|
} catch (error) { |
|
throw makeError({ |
|
error, |
|
stdout: '', |
|
stderr: '', |
|
all: '', |
|
command, |
|
escapedCommand, |
|
parsed, |
|
timedOut: false, |
|
isCanceled: false, |
|
killed: false, |
|
}); |
|
} |
|
|
|
const stdout = handleOutput(parsed.options, result.stdout, result.error); |
|
const stderr = handleOutput(parsed.options, result.stderr, result.error); |
|
|
|
if (result.error || result.status !== 0 || result.signal !== null) { |
|
const error = makeError({ |
|
stdout, |
|
stderr, |
|
error: result.error, |
|
signal: result.signal, |
|
exitCode: result.status, |
|
command, |
|
escapedCommand, |
|
parsed, |
|
timedOut: result.error && result.error.code === 'ETIMEDOUT', |
|
isCanceled: false, |
|
killed: result.signal !== null, |
|
}); |
|
|
|
if (!parsed.options.reject) { |
|
return error; |
|
} |
|
|
|
throw error; |
|
} |
|
|
|
return { |
|
command, |
|
escapedCommand, |
|
exitCode: 0, |
|
stdout, |
|
stderr, |
|
failed: false, |
|
timedOut: false, |
|
isCanceled: false, |
|
killed: false, |
|
}; |
|
} |
|
|
|
const normalizeScriptStdin = ({input, inputFile, stdio}) => input === undefined && inputFile === undefined && stdio === undefined |
|
? {stdin: 'inherit'} |
|
: {}; |
|
|
|
const normalizeScriptOptions = (options = {}) => ({ |
|
preferLocal: true, |
|
...normalizeScriptStdin(options), |
|
...options, |
|
}); |
|
|
|
function create$(options) { |
|
function $(templatesOrOptions, ...expressions) { |
|
if (!Array.isArray(templatesOrOptions)) { |
|
return create$({...options, ...templatesOrOptions}); |
|
} |
|
|
|
const [file, ...args] = parseTemplates(templatesOrOptions, expressions); |
|
return execa(file, args, normalizeScriptOptions(options)); |
|
} |
|
|
|
$.sync = (templates, ...expressions) => { |
|
if (!Array.isArray(templates)) { |
|
throw new TypeError('Please use $(options).sync`command` instead of $.sync(options)`command`.'); |
|
} |
|
|
|
const [file, ...args] = parseTemplates(templates, expressions); |
|
return execaSync(file, args, normalizeScriptOptions(options)); |
|
}; |
|
|
|
return $; |
|
} |
|
|
|
export const $ = create$(); |
|
|
|
export function execaCommand(command, options) { |
|
const [file, ...args] = parseCommand(command); |
|
return execa(file, args, options); |
|
} |
|
|
|
export function execaCommandSync(command, options) { |
|
const [file, ...args] = parseCommand(command); |
|
return execaSync(file, args, options); |
|
} |
|
|
|
export function execaNode(scriptPath, args, options = {}) { |
|
if (args && !Array.isArray(args) && typeof args === 'object') { |
|
options = args; |
|
args = []; |
|
} |
|
|
|
const stdio = normalizeStdioNode(options); |
|
const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); |
|
|
|
const { |
|
nodePath = process.execPath, |
|
nodeOptions = defaultExecArgv, |
|
} = options; |
|
|
|
return execa( |
|
nodePath, |
|
[ |
|
...nodeOptions, |
|
scriptPath, |
|
...(Array.isArray(args) ? args : []), |
|
], |
|
{ |
|
...options, |
|
stdin: undefined, |
|
stdout: undefined, |
|
stderr: undefined, |
|
stdio, |
|
shell: false, |
|
}, |
|
); |
|
} |
|
|