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  };