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') { // #116 args.unshift('/q'); } return {file, args, options, parsed}; }; const handleOutput = (options, value, error) => { if (typeof value !== 'string' && !Buffer.isBuffer(value)) { // When `execaSync()` errors, we normalize it to '' to mimic `execa()` 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) { // Ensure the returned error is always both a promise and a child process 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, }, ); }