simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

index.js (6516B)


      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, validateTimeout, setExitHandler} = require('./lib/kill');
     11 const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = require('./lib/stream');
     12 const {mergePromise, getSpawnedPromise} = require('./lib/promise');
     13 const {joinCommand, parseCommand, getEscapedCommand} = require('./lib/command');
     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 	const escapedCommand = getEscapedCommand(file, args);
     78 
     79 	validateTimeout(parsed.options);
     80 
     81 	let spawned;
     82 	try {
     83 		spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options);
     84 	} catch (error) {
     85 		// Ensure the returned error is always both a promise and a child process
     86 		const dummySpawned = new childProcess.ChildProcess();
     87 		const errorPromise = Promise.reject(makeError({
     88 			error,
     89 			stdout: '',
     90 			stderr: '',
     91 			all: '',
     92 			command,
     93 			escapedCommand,
     94 			parsed,
     95 			timedOut: false,
     96 			isCanceled: false,
     97 			killed: false
     98 		}));
     99 		return mergePromise(dummySpawned, errorPromise);
    100 	}
    101 
    102 	const spawnedPromise = getSpawnedPromise(spawned);
    103 	const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise);
    104 	const processDone = setExitHandler(spawned, parsed.options, timedPromise);
    105 
    106 	const context = {isCanceled: false};
    107 
    108 	spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned));
    109 	spawned.cancel = spawnedCancel.bind(null, spawned, context);
    110 
    111 	const handlePromise = async () => {
    112 		const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone);
    113 		const stdout = handleOutput(parsed.options, stdoutResult);
    114 		const stderr = handleOutput(parsed.options, stderrResult);
    115 		const all = handleOutput(parsed.options, allResult);
    116 
    117 		if (error || exitCode !== 0 || signal !== null) {
    118 			const returnedError = makeError({
    119 				error,
    120 				exitCode,
    121 				signal,
    122 				stdout,
    123 				stderr,
    124 				all,
    125 				command,
    126 				escapedCommand,
    127 				parsed,
    128 				timedOut,
    129 				isCanceled: context.isCanceled,
    130 				killed: spawned.killed
    131 			});
    132 
    133 			if (!parsed.options.reject) {
    134 				return returnedError;
    135 			}
    136 
    137 			throw returnedError;
    138 		}
    139 
    140 		return {
    141 			command,
    142 			escapedCommand,
    143 			exitCode: 0,
    144 			stdout,
    145 			stderr,
    146 			all,
    147 			failed: false,
    148 			timedOut: false,
    149 			isCanceled: false,
    150 			killed: false
    151 		};
    152 	};
    153 
    154 	const handlePromiseOnce = onetime(handlePromise);
    155 
    156 	handleInput(spawned, parsed.options.input);
    157 
    158 	spawned.all = makeAllStream(spawned, parsed.options);
    159 
    160 	return mergePromise(spawned, handlePromiseOnce);
    161 };
    162 
    163 module.exports = execa;
    164 
    165 module.exports.sync = (file, args, options) => {
    166 	const parsed = handleArguments(file, args, options);
    167 	const command = joinCommand(file, args);
    168 	const escapedCommand = getEscapedCommand(file, args);
    169 
    170 	validateInputSync(parsed.options);
    171 
    172 	let result;
    173 	try {
    174 		result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options);
    175 	} catch (error) {
    176 		throw makeError({
    177 			error,
    178 			stdout: '',
    179 			stderr: '',
    180 			all: '',
    181 			command,
    182 			escapedCommand,
    183 			parsed,
    184 			timedOut: false,
    185 			isCanceled: false,
    186 			killed: false
    187 		});
    188 	}
    189 
    190 	const stdout = handleOutput(parsed.options, result.stdout, result.error);
    191 	const stderr = handleOutput(parsed.options, result.stderr, result.error);
    192 
    193 	if (result.error || result.status !== 0 || result.signal !== null) {
    194 		const error = makeError({
    195 			stdout,
    196 			stderr,
    197 			error: result.error,
    198 			signal: result.signal,
    199 			exitCode: result.status,
    200 			command,
    201 			escapedCommand,
    202 			parsed,
    203 			timedOut: result.error && result.error.code === 'ETIMEDOUT',
    204 			isCanceled: false,
    205 			killed: result.signal !== null
    206 		});
    207 
    208 		if (!parsed.options.reject) {
    209 			return error;
    210 		}
    211 
    212 		throw error;
    213 	}
    214 
    215 	return {
    216 		command,
    217 		escapedCommand,
    218 		exitCode: 0,
    219 		stdout,
    220 		stderr,
    221 		failed: false,
    222 		timedOut: false,
    223 		isCanceled: false,
    224 		killed: false
    225 	};
    226 };
    227 
    228 module.exports.command = (command, options) => {
    229 	const [file, ...args] = parseCommand(command);
    230 	return execa(file, args, options);
    231 };
    232 
    233 module.exports.commandSync = (command, options) => {
    234 	const [file, ...args] = parseCommand(command);
    235 	return execa.sync(file, args, options);
    236 };
    237 
    238 module.exports.node = (scriptPath, args, options = {}) => {
    239 	if (args && !Array.isArray(args) && typeof args === 'object') {
    240 		options = args;
    241 		args = [];
    242 	}
    243 
    244 	const stdio = normalizeStdio.node(options);
    245 	const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect'));
    246 
    247 	const {
    248 		nodePath = process.execPath,
    249 		nodeOptions = defaultExecArgv
    250 	} = options;
    251 
    252 	return execa(
    253 		nodePath,
    254 		[
    255 			...nodeOptions,
    256 			scriptPath,
    257 			...(Array.isArray(args) ? args : [])
    258 		],
    259 		{
    260 			...options,
    261 			stdin: undefined,
    262 			stdout: undefined,
    263 			stderr: undefined,
    264 			stdio,
    265 			shell: false
    266 		}
    267 	);
    268 };