310 lines
7.7 KiB
JavaScript
310 lines
7.7 KiB
JavaScript
|
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,
|
||
|
},
|
||
|
);
|
||
|
}
|