215 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			215 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {getStreamName, isStandardStream} from '../utils/standard-stream.js';
 | |
| import {normalizeTransforms} from '../transform/normalize.js';
 | |
| import {getFdObjectMode} from '../transform/object-mode.js';
 | |
| import {
 | |
| 	getStdioItemType,
 | |
| 	isRegularUrl,
 | |
| 	isUnknownStdioString,
 | |
| 	FILE_TYPES,
 | |
| } from './type.js';
 | |
| import {getStreamDirection} from './direction.js';
 | |
| import {normalizeStdioOption} from './stdio-option.js';
 | |
| import {handleNativeStream} from './native.js';
 | |
| import {handleInputOptions} from './input-option.js';
 | |
| import {filterDuplicates, getDuplicateStream} from './duplicate.js';
 | |
| 
 | |
| // Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async/sync mode
 | |
| // They are converted into an array of `fileDescriptors`.
 | |
| // Each `fileDescriptor` is normalized, validated and contains all information necessary for further handling.
 | |
| export const handleStdio = (addProperties, options, verboseInfo, isSync) => {
 | |
| 	const stdio = normalizeStdioOption(options, verboseInfo, isSync);
 | |
| 	const initialFileDescriptors = stdio.map((stdioOption, fdNumber) => getFileDescriptor({
 | |
| 		stdioOption,
 | |
| 		fdNumber,
 | |
| 		options,
 | |
| 		isSync,
 | |
| 	}));
 | |
| 	const fileDescriptors = getFinalFileDescriptors({
 | |
| 		initialFileDescriptors,
 | |
| 		addProperties,
 | |
| 		options,
 | |
| 		isSync,
 | |
| 	});
 | |
| 	options.stdio = fileDescriptors.map(({stdioItems}) => forwardStdio(stdioItems));
 | |
| 	return fileDescriptors;
 | |
| };
 | |
| 
 | |
| const getFileDescriptor = ({stdioOption, fdNumber, options, isSync}) => {
 | |
| 	const optionName = getStreamName(fdNumber);
 | |
| 	const {stdioItems: initialStdioItems, isStdioArray} = initializeStdioItems({
 | |
| 		stdioOption,
 | |
| 		fdNumber,
 | |
| 		options,
 | |
| 		optionName,
 | |
| 	});
 | |
| 	const direction = getStreamDirection(initialStdioItems, fdNumber, optionName);
 | |
| 	const stdioItems = initialStdioItems.map(stdioItem => handleNativeStream({
 | |
| 		stdioItem,
 | |
| 		isStdioArray,
 | |
| 		fdNumber,
 | |
| 		direction,
 | |
| 		isSync,
 | |
| 	}));
 | |
| 	const normalizedStdioItems = normalizeTransforms(stdioItems, optionName, direction, options);
 | |
| 	const objectMode = getFdObjectMode(normalizedStdioItems, direction);
 | |
| 	validateFileObjectMode(normalizedStdioItems, objectMode);
 | |
| 	return {direction, objectMode, stdioItems: normalizedStdioItems};
 | |
| };
 | |
| 
 | |
| // We make sure passing an array with a single item behaves the same as passing that item without an array.
 | |
| // This is what users would expect.
 | |
| // For example, `stdout: ['ignore']` behaves the same as `stdout: 'ignore'`.
 | |
| const initializeStdioItems = ({stdioOption, fdNumber, options, optionName}) => {
 | |
| 	const values = Array.isArray(stdioOption) ? stdioOption : [stdioOption];
 | |
| 	const initialStdioItems = [
 | |
| 		...values.map(value => initializeStdioItem(value, optionName)),
 | |
| 		...handleInputOptions(options, fdNumber),
 | |
| 	];
 | |
| 
 | |
| 	const stdioItems = filterDuplicates(initialStdioItems);
 | |
| 	const isStdioArray = stdioItems.length > 1;
 | |
| 	validateStdioArray(stdioItems, isStdioArray, optionName);
 | |
| 	validateStreams(stdioItems);
 | |
| 	return {stdioItems, isStdioArray};
 | |
| };
 | |
| 
 | |
| const initializeStdioItem = (value, optionName) => ({
 | |
| 	type: getStdioItemType(value, optionName),
 | |
| 	value,
 | |
| 	optionName,
 | |
| });
 | |
| 
 | |
| const validateStdioArray = (stdioItems, isStdioArray, optionName) => {
 | |
| 	if (stdioItems.length === 0) {
 | |
| 		throw new TypeError(`The \`${optionName}\` option must not be an empty array.`);
 | |
| 	}
 | |
| 
 | |
| 	if (!isStdioArray) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	for (const {value, optionName} of stdioItems) {
 | |
| 		if (INVALID_STDIO_ARRAY_OPTIONS.has(value)) {
 | |
| 			throw new Error(`The \`${optionName}\` option must not include \`${value}\`.`);
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| // Using those `stdio` values together with others for the same stream does not make sense, so we make it fail.
 | |
| // However, we do allow it if the array has a single item.
 | |
| const INVALID_STDIO_ARRAY_OPTIONS = new Set(['ignore', 'ipc']);
 | |
| 
 | |
| const validateStreams = stdioItems => {
 | |
| 	for (const stdioItem of stdioItems) {
 | |
| 		validateFileStdio(stdioItem);
 | |
| 	}
 | |
| };
 | |
| 
 | |
| const validateFileStdio = ({type, value, optionName}) => {
 | |
| 	if (isRegularUrl(value)) {
 | |
| 		throw new TypeError(`The \`${optionName}: URL\` option must use the \`file:\` scheme.
 | |
| For example, you can use the \`pathToFileURL()\` method of the \`url\` core module.`);
 | |
| 	}
 | |
| 
 | |
| 	if (isUnknownStdioString(type, value)) {
 | |
| 		throw new TypeError(`The \`${optionName}: { file: '...' }\` option must be used instead of \`${optionName}: '...'\`.`);
 | |
| 	}
 | |
| };
 | |
| 
 | |
| const validateFileObjectMode = (stdioItems, objectMode) => {
 | |
| 	if (!objectMode) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const fileStdioItem = stdioItems.find(({type}) => FILE_TYPES.has(type));
 | |
| 	if (fileStdioItem !== undefined) {
 | |
| 		throw new TypeError(`The \`${fileStdioItem.optionName}\` option cannot use both files and transforms in objectMode.`);
 | |
| 	}
 | |
| };
 | |
| 
 | |
| // Some `stdio` values require Execa to create streams.
 | |
| // For example, file paths create file read/write streams.
 | |
| // Those transformations are specified in `addProperties`, which is both direction-specific and type-specific.
 | |
| const getFinalFileDescriptors = ({initialFileDescriptors, addProperties, options, isSync}) => {
 | |
| 	const fileDescriptors = [];
 | |
| 
 | |
| 	try {
 | |
| 		for (const fileDescriptor of initialFileDescriptors) {
 | |
| 			fileDescriptors.push(getFinalFileDescriptor({
 | |
| 				fileDescriptor,
 | |
| 				fileDescriptors,
 | |
| 				addProperties,
 | |
| 				options,
 | |
| 				isSync,
 | |
| 			}));
 | |
| 		}
 | |
| 
 | |
| 		return fileDescriptors;
 | |
| 	} catch (error) {
 | |
| 		cleanupCustomStreams(fileDescriptors);
 | |
| 		throw error;
 | |
| 	}
 | |
| };
 | |
| 
 | |
| const getFinalFileDescriptor = ({
 | |
| 	fileDescriptor: {direction, objectMode, stdioItems},
 | |
| 	fileDescriptors,
 | |
| 	addProperties,
 | |
| 	options,
 | |
| 	isSync,
 | |
| }) => {
 | |
| 	const finalStdioItems = stdioItems.map(stdioItem => addStreamProperties({
 | |
| 		stdioItem,
 | |
| 		addProperties,
 | |
| 		direction,
 | |
| 		options,
 | |
| 		fileDescriptors,
 | |
| 		isSync,
 | |
| 	}));
 | |
| 	return {direction, objectMode, stdioItems: finalStdioItems};
 | |
| };
 | |
| 
 | |
| const addStreamProperties = ({stdioItem, addProperties, direction, options, fileDescriptors, isSync}) => {
 | |
| 	const duplicateStream = getDuplicateStream({
 | |
| 		stdioItem,
 | |
| 		direction,
 | |
| 		fileDescriptors,
 | |
| 		isSync,
 | |
| 	});
 | |
| 
 | |
| 	if (duplicateStream !== undefined) {
 | |
| 		return {...stdioItem, stream: duplicateStream};
 | |
| 	}
 | |
| 
 | |
| 	return {
 | |
| 		...stdioItem,
 | |
| 		...addProperties[direction][stdioItem.type](stdioItem, options),
 | |
| 	};
 | |
| };
 | |
| 
 | |
| // The stream error handling is performed by the piping logic above, which cannot be performed before subprocess spawning.
 | |
| // If the subprocess spawning fails (e.g. due to an invalid command), the streams need to be manually destroyed.
 | |
| // We need to create those streams before subprocess spawning, in case their creation fails, e.g. when passing an invalid generator as argument.
 | |
| // Like this, an exception would be thrown, which would prevent spawning a subprocess.
 | |
| export const cleanupCustomStreams = fileDescriptors => {
 | |
| 	for (const {stdioItems} of fileDescriptors) {
 | |
| 		for (const {stream} of stdioItems) {
 | |
| 			if (stream !== undefined && !isStandardStream(stream)) {
 | |
| 				stream.destroy();
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| // When the `std*: Iterable | WebStream | URL | filePath`, `input` or `inputFile` option is used, we pipe to `subprocess.std*`.
 | |
| // When the `std*: Array` option is used, we emulate some of the native values ('inherit', Node.js stream and file descriptor integer). To do so, we also need to pipe to `subprocess.std*`.
 | |
| // Therefore the `std*` options must be either `pipe` or `overlapped`. Other values do not set `subprocess.std*`.
 | |
| const forwardStdio = stdioItems => {
 | |
| 	if (stdioItems.length > 1) {
 | |
| 		return stdioItems.some(({value}) => value === 'overlapped') ? 'overlapped' : 'pipe';
 | |
| 	}
 | |
| 
 | |
| 	const [{type, value}] = stdioItems;
 | |
| 	return type === 'native' ? value : 'pipe';
 | |
| };
 |