init project

This commit is contained in:
root
2026-07-31 13:12:54 -04:00
parent 0da92d5e02
commit f3863f760c
7215 changed files with 1860260 additions and 1 deletions
+89
View File
@@ -0,0 +1,89 @@
import process from 'node:process';
import {
isStream as isNodeStream,
isReadableStream as isNodeReadableStream,
isWritableStream as isNodeWritableStream,
} from 'is-stream';
import {isWritableStream} from './type.js';
// For `stdio[fdNumber]` beyond stdin/stdout/stderr, we need to guess whether the value passed is intended for inputs or outputs.
// This allows us to know whether to pipe _into_ or _from_ the stream.
// When `stdio[fdNumber]` is a single value, this guess is fairly straightforward.
// However, when it is an array instead, we also need to make sure the different values are not incompatible with each other.
export const getStreamDirection = (stdioItems, fdNumber, optionName) => {
const directions = stdioItems.map(stdioItem => getStdioItemDirection(stdioItem, fdNumber));
if (directions.includes('input') && directions.includes('output')) {
throw new TypeError(`The \`${optionName}\` option must not be an array of both readable and writable values.`);
}
return directions.find(Boolean) ?? DEFAULT_DIRECTION;
};
// `direction` is set when the user explicitly requests it, e.g. `{value: 'pipe', input: true}`.
// It takes precedence over guessing, but not over the fixed direction of `stdin`/`stdout`/`stderr`.
const getStdioItemDirection = (stdioItem, fdNumber) => KNOWN_DIRECTIONS[fdNumber] ?? getRequestedDirection(stdioItem);
// The explicitly requested `direction` must not contradict a value that is intrinsically the other direction.
// For example, `{value: writableStream, input: true}` is invalid since a writable value can only be an output.
const getRequestedDirection = ({type, value, direction, optionName}) => {
const guessedDirection = guessStreamDirection[type](value);
if (direction === 'input' && guessedDirection === 'output') {
throw new TypeError(`The \`${optionName}\` option is invalid: \`input: true\` cannot be used with a writable value, which is always an output.`);
}
return direction ?? guessedDirection;
};
// `stdin`/`stdout`/`stderr` have a known direction
const KNOWN_DIRECTIONS = ['input', 'output', 'output'];
const anyDirection = () => undefined;
const alwaysInput = () => 'input';
// `string` can only be added through the `input` option, i.e. does not need to be handled here
const guessStreamDirection = {
generator: anyDirection,
asyncGenerator: anyDirection,
fileUrl: anyDirection,
filePath: anyDirection,
iterable: alwaysInput,
asyncIterable: alwaysInput,
uint8Array: alwaysInput,
webStream: value => isWritableStream(value) ? 'output' : 'input',
nodeStream(value) {
if (!isNodeReadableStream(value, {checkOpen: false})) {
return 'output';
}
return isNodeWritableStream(value, {checkOpen: false}) ? undefined : 'input';
},
webTransform: anyDirection,
duplex: anyDirection,
native(value) {
const standardStreamDirection = getStandardStreamDirection(value);
if (standardStreamDirection !== undefined) {
return standardStreamDirection;
}
if (isNodeStream(value, {checkOpen: false})) {
return guessStreamDirection.nodeStream(value);
}
},
};
const getStandardStreamDirection = value => {
if ([0, process.stdin].includes(value)) {
return 'input';
}
if ([1, 2, process.stdout, process.stderr].includes(value)) {
return 'output';
}
};
// When ambiguous, we initially keep the direction as `undefined`.
// This allows arrays of `stdio` values to resolve the ambiguity.
// For example, `stdio[3]: DuplexStream` is ambiguous, but `stdio[3]: [DuplexStream, WritableStream]` is not.
// When the ambiguity remains, we default to `output` since it is the most common use case for additional file descriptors.
const DEFAULT_DIRECTION = 'output';
+119
View File
@@ -0,0 +1,119 @@
import {
SPECIAL_DUPLICATE_TYPES_SYNC,
SPECIAL_DUPLICATE_TYPES,
FORBID_DUPLICATE_TYPES,
TYPE_TO_MESSAGE,
} from './type.js';
// Duplicates in the same file descriptor is most likely an error.
// However, this can be useful with generators.
export const filterDuplicates = stdioItems => stdioItems.filter((stdioItemOne, indexOne) =>
stdioItems.every((stdioItemTwo, indexTwo) => !hasSameValueAndDirection(stdioItemOne, stdioItemTwo)
|| indexOne >= indexTwo
|| stdioItemOne.type === 'generator'
|| stdioItemOne.type === 'asyncGenerator'));
const hasSameValueAndDirection = (stdioItemOne, stdioItemTwo) => stdioItemOne.value === stdioItemTwo.value
&& stdioItemOne.direction === stdioItemTwo.direction;
// Check if two file descriptors are sharing the same target.
// For example `{stdout: {file: './output.txt'}, stderr: {file: './output.txt'}}`.
export const getDuplicateStream = ({stdioItem: {type, value, optionName}, direction, fileDescriptors, isSync}) => {
const otherStdioItems = getOtherStdioItems(fileDescriptors, type);
if (otherStdioItems.length === 0) {
return;
}
if (isSync) {
validateDuplicateStreamSync({
otherStdioItems,
type,
value,
optionName,
direction,
});
return;
}
if (SPECIAL_DUPLICATE_TYPES.has(type)) {
return getDuplicateStreamInstance({
otherStdioItems,
type,
value,
optionName,
direction,
});
}
if (FORBID_DUPLICATE_TYPES.has(type)) {
validateDuplicateTransform({
otherStdioItems,
type,
value,
optionName,
});
}
};
// Values shared by multiple file descriptors
const getOtherStdioItems = (fileDescriptors, type) => fileDescriptors
.flatMap(({direction, stdioItems}) => stdioItems
.filter(stdioItem => stdioItem.type === type)
.map((stdioItem => ({...stdioItem, direction}))));
// With `execaSync()`, do not allow setting a file path both in input and output
const validateDuplicateStreamSync = ({otherStdioItems, type, value, optionName, direction}) => {
if (SPECIAL_DUPLICATE_TYPES_SYNC.has(type)) {
getDuplicateStreamInstance({
otherStdioItems,
type,
value,
optionName,
direction,
});
}
};
// When two file descriptors share the file or stream, we need to re-use the same underlying stream.
// Otherwise, the stream would be closed twice when piping ends.
// This is only an issue with output file descriptors.
// This is not a problem with generator functions since those create a new instance for each file descriptor.
// We also forbid input and output file descriptors sharing the same file or stream, since that does not make sense.
const getDuplicateStreamInstance = ({otherStdioItems, type, value, optionName, direction}) => {
const duplicateStdioItems = otherStdioItems.filter(stdioItem => hasSameValue(stdioItem, value));
if (duplicateStdioItems.length === 0) {
return;
}
const differentStdioItem = duplicateStdioItems.find(stdioItem => stdioItem.direction !== direction);
throwOnDuplicateStream(differentStdioItem, optionName, type);
return direction === 'output' ? duplicateStdioItems[0].stream : undefined;
};
const hasSameValue = ({type, value}, secondValue) => {
if (type === 'filePath') {
return value.file === secondValue.file;
}
if (type === 'fileUrl') {
return value.href === secondValue.href;
}
return value === secondValue;
};
// We do not allow two file descriptors to share the same Duplex or TransformStream.
// This is because those are set directly to `subprocess.std*`.
// For example, this could result in `subprocess.stdout` and `subprocess.stderr` being the same value.
// This means reading from either would get data from both stdout and stderr.
const validateDuplicateTransform = ({otherStdioItems, type, value, optionName}) => {
const duplicateStdioItem = otherStdioItems.find(({value: {transform}}) => transform === value.transform);
throwOnDuplicateStream(duplicateStdioItem, optionName, type);
};
const throwOnDuplicateStream = (stdioItem, optionName, type) => {
if (stdioItem !== undefined) {
throw new TypeError(`The \`${stdioItem.optionName}\` and \`${optionName}\` options must not target ${TYPE_TO_MESSAGE[type]} that is the same.`);
}
};
+52
View File
@@ -0,0 +1,52 @@
import {createReadStream, createWriteStream} from 'node:fs';
import {Buffer} from 'node:buffer';
import {Readable, Writable, Duplex} from 'node:stream';
import {generatorToStream} from '../transform/generator.js';
import {handleStdio} from './handle.js';
import {TYPE_TO_MESSAGE} from './type.js';
// Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async mode
export const handleStdioAsync = (options, verboseInfo) => handleStdio(addPropertiesAsync, options, verboseInfo, false);
const forbiddenIfAsync = ({type, optionName}) => {
throw new TypeError(`The \`${optionName}\` option cannot be ${TYPE_TO_MESSAGE[type]}.`);
};
// Create streams used internally for piping when using specific values for the `std*` options, in async mode.
// For example, `stdout: {file}` creates a file stream, which is piped from/to.
const addProperties = {
fileNumber: forbiddenIfAsync,
generator: generatorToStream,
asyncGenerator: generatorToStream,
nodeStream: ({value}) => ({stream: value}),
webTransform({value: {transform, writableObjectMode, readableObjectMode}}) {
const objectMode = writableObjectMode || readableObjectMode;
const stream = Duplex.fromWeb(transform, {objectMode});
return {stream};
},
duplex: ({value: {transform}}) => ({stream: transform}),
native() {},
};
const addPropertiesAsync = {
input: {
...addProperties,
fileUrl: ({value}) => ({stream: createReadStream(value)}),
filePath: ({value: {file}}) => ({stream: createReadStream(file)}),
webStream: ({value}) => ({stream: Readable.fromWeb(value)}),
iterable: ({value}) => ({stream: Readable.from(value)}),
asyncIterable: ({value}) => ({stream: Readable.from(value)}),
string: ({value}) => ({stream: Readable.from(value)}),
uint8Array: ({value}) => ({stream: Readable.from(Buffer.from(value))}),
},
output: {
...addProperties,
fileUrl: ({value}) => ({stream: createWriteStream(value)}),
filePath: ({value: {file, append}}) => ({stream: createWriteStream(file, append ? {flags: 'a'} : {})}),
webStream: ({value}) => ({stream: Writable.fromWeb(value)}),
iterable: forbiddenIfAsync,
asyncIterable: forbiddenIfAsync,
string: forbiddenIfAsync,
uint8Array: forbiddenIfAsync,
},
};
+68
View File
@@ -0,0 +1,68 @@
import {readFileSync} from 'node:fs';
import {bufferToUint8Array} from '../utils/uint-array.js';
import {handleStdio} from './handle.js';
import {TYPE_TO_MESSAGE} from './type.js';
// Normalize `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in sync mode
export const handleStdioSync = (options, verboseInfo) => handleStdio(addPropertiesSync, options, verboseInfo, true);
const forbiddenIfSync = ({type, optionName}) => {
throwInvalidSyncValue(optionName, TYPE_TO_MESSAGE[type]);
};
const forbiddenNativeIfSync = ({optionName, value}) => {
if (value === 'overlapped') {
throwInvalidSyncValue(optionName, `"${value}"`);
}
return {};
};
// A native input pipe on an additional file descriptor cannot be fed in sync mode, since there is no way to write to it after spawning, so it would hang.
const forbiddenNativeInputIfSync = stdioItem => {
const {optionName, value} = stdioItem;
if (value === 'pipe' && optionName !== 'stdin') {
throw new TypeError(`Only the \`stdin\` option, not \`${optionName}\`, can be an input pipe with synchronous methods.`);
}
return forbiddenNativeIfSync(stdioItem);
};
const throwInvalidSyncValue = (optionName, value) => {
throw new TypeError(`The \`${optionName}\` option cannot be ${value} with synchronous methods.`);
};
// Create streams used internally for redirecting when using specific values for the `std*` options, in sync mode.
// For example, `stdin: {file}` reads the file synchronously, then passes it as the `input` option.
const addProperties = {
generator() {},
asyncGenerator: forbiddenIfSync,
webStream: forbiddenIfSync,
nodeStream: forbiddenIfSync,
webTransform: forbiddenIfSync,
duplex: forbiddenIfSync,
asyncIterable: forbiddenIfSync,
native: forbiddenNativeIfSync,
};
const addPropertiesSync = {
input: {
...addProperties,
native: forbiddenNativeInputIfSync,
fileUrl: ({value}) => ({contents: [bufferToUint8Array(readFileSync(value))]}),
filePath: ({value: {file}}) => ({contents: [bufferToUint8Array(readFileSync(file))]}),
fileNumber: forbiddenIfSync,
iterable: ({value}) => ({contents: [...value]}),
string: ({value}) => ({contents: [value]}),
uint8Array: ({value}) => ({contents: [value]}),
},
output: {
...addProperties,
fileUrl: ({value}) => ({path: value}),
filePath: ({value: {file, append}}) => ({path: file, append}),
fileNumber: ({value}) => ({path: value}),
iterable: forbiddenIfSync,
string: forbiddenIfSync,
uint8Array: forbiddenIfSync,
},
};
+243
View File
@@ -0,0 +1,243 @@
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,
isStdioValueObject,
checkBooleanOption,
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 inputStdioItems = handleInputOptions(options, fdNumber);
const initialStdioItems = [
...omitInheritedStdin(values.map(value => initializeStdioItem(value, optionName)), inputStdioItems),
...inputStdioItems,
];
const stdioItems = filterDuplicates(initialStdioItems);
const isStdioArray = stdioItems.length > 1;
validateStdioArray(stdioItems, isStdioArray, optionName);
validateStreams(stdioItems);
return {stdioItems, isStdioArray};
};
const omitInheritedStdin = (stdioItems, inputStdioItems) => inputStdioItems.length > 0 && isInheritedStdinOnly(stdioItems)
? []
: stdioItems;
const isInheritedStdinOnly = stdioItems => stdioItems.length === 1
&& stdioItems[0].type === 'native'
&& stdioItems[0].value === 'inherit';
const initializeStdioItem = (value, optionName) => {
if (isStdioValueObject(value)) {
return initializeStdioValueObject(value, optionName);
}
return {
type: getStdioItemType(value, optionName),
value,
optionName,
};
};
// The `{value, input}` object form wraps any stdio value with an explicit direction.
const initializeStdioValueObject = ({value, input}, optionName) => {
checkBooleanOption(input, `${optionName}.input`);
return {
type: getStdioItemType(value, optionName),
value,
direction: input ? 'input' : undefined,
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 the `ignore` value 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']);
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';
};
+54
View File
@@ -0,0 +1,54 @@
import {isReadableStream} from 'is-stream';
import {isUint8Array} from '../utils/uint-array.js';
import {isUrl, isFilePathString} from './type.js';
// Append the `stdin` option with the `input` and `inputFile` options
export const handleInputOptions = ({input, inputFile}, fdNumber) => fdNumber === 0
? [
...handleInputOption(input),
...handleInputFileOption(inputFile),
]
: [];
const handleInputOption = input => input === undefined
? []
: [{
type: getInputType(input),
value: input,
optionName: 'input',
}];
const getInputType = input => {
if (isReadableStream(input, {checkOpen: false})) {
return 'nodeStream';
}
if (typeof input === 'string') {
return 'string';
}
if (isUint8Array(input)) {
return 'uint8Array';
}
throw new Error('The `input` option must be a string, a Uint8Array or a Node.js Readable stream.');
};
const handleInputFileOption = inputFile => inputFile === undefined
? []
: [{
...getInputFileType(inputFile),
optionName: 'inputFile',
}];
const getInputFileType = inputFile => {
if (isUrl(inputFile)) {
return {type: 'fileUrl', value: inputFile};
}
if (isFilePathString(inputFile)) {
return {type: 'filePath', value: {file: inputFile}};
}
throw new Error('The `inputFile` option must be a file path string or a file URL.');
};
+106
View File
@@ -0,0 +1,106 @@
import {readFileSync} from 'node:fs';
import tty from 'node:tty';
import {isStream as isNodeStream} from 'is-stream';
import {STANDARD_STREAMS} from '../utils/standard-stream.js';
import {bufferToUint8Array} from '../utils/uint-array.js';
import {serializeOptionValue} from '../arguments/fd-options.js';
// When we use multiple `stdio` values for the same streams, we pass 'pipe' to `child_process.spawn()`.
// We then emulate the piping done by core Node.js.
// To do so, we transform the following values:
// - Node.js streams are marked as `type: nodeStream`
// - 'inherit' becomes `process.stdin|stdout|stderr`
// - any file descriptor integer becomes `process.stdio[fdNumber]`
// All of the above transformations tell Execa to perform manual piping.
export const handleNativeStream = ({stdioItem, stdioItem: {type}, isStdioArray, fdNumber, direction, isSync}) => {
if (!isStdioArray || type !== 'native') {
return stdioItem;
}
return isSync
? handleNativeStreamSync({stdioItem, fdNumber, direction})
: handleNativeStreamAsync({stdioItem, fdNumber});
};
// Synchronous methods use a different logic.
// 'inherit', file descriptors and process.std* are handled by readFileSync()/writeFileSync().
const handleNativeStreamSync = ({stdioItem, stdioItem: {value, optionName}, fdNumber, direction}) => {
const targetFd = getTargetFd({
value,
optionName,
fdNumber,
direction,
});
if (targetFd !== undefined) {
return targetFd;
}
if (isNodeStream(value, {checkOpen: false})) {
throw new TypeError(`The \`${optionName}: Stream\` option cannot both be an array and include a stream with synchronous methods.`);
}
return stdioItem;
};
const getTargetFd = ({value, optionName, fdNumber, direction}) => {
const targetFdNumber = getTargetFdNumber(value, fdNumber);
if (targetFdNumber === undefined) {
return;
}
if (direction === 'output') {
return {type: 'fileNumber', value: targetFdNumber, optionName};
}
if (tty.isatty(targetFdNumber)) {
throw new TypeError(`The \`${optionName}: ${serializeOptionValue(value)}\` option is invalid: it cannot be a TTY with synchronous methods.`);
}
return {type: 'uint8Array', value: bufferToUint8Array(readFileSync(targetFdNumber)), optionName};
};
const getTargetFdNumber = (value, fdNumber) => {
if (value === 'inherit') {
return fdNumber;
}
if (typeof value === 'number') {
return value;
}
const standardStreamIndex = STANDARD_STREAMS.indexOf(value);
if (standardStreamIndex !== -1) {
return standardStreamIndex;
}
};
const handleNativeStreamAsync = ({stdioItem, stdioItem: {value, optionName}, fdNumber}) => {
if (value === 'inherit') {
return {type: 'nodeStream', value: getStandardStream(fdNumber, value, optionName), optionName};
}
if (typeof value === 'number') {
return {type: 'nodeStream', value: getStandardStream(value, value, optionName), optionName};
}
if (isNodeStream(value, {checkOpen: false})) {
return {type: 'nodeStream', value, optionName};
}
return stdioItem;
};
// Node.js does not allow to easily retrieve file descriptors beyond stdin/stdout/stderr as streams.
// - `fs.createReadStream()`/`fs.createWriteStream()` with the `fd` option do not work with character devices that use blocking reads/writes (such as interactive TTYs).
// - Using a TCP `Socket` would work but be rather complex to implement.
// Since this is an edge case, we simply throw an error message.
// See https://github.com/sindresorhus/execa/pull/643#discussion_r1435905707
const getStandardStream = (fdNumber, value, optionName) => {
const standardStream = STANDARD_STREAMS[fdNumber];
if (standardStream === undefined) {
throw new TypeError(`The \`${optionName}: ${value}\` option is invalid: no such standard stream.`);
}
return standardStream;
};
+95
View File
@@ -0,0 +1,95 @@
import {STANDARD_STREAMS_ALIASES} from '../utils/standard-stream.js';
import {normalizeIpcStdioArray} from '../ipc/array.js';
import {isFullVerbose} from '../verbose/values.js';
import {isStdioValueObject} from './type.js';
// Add support for `stdin`/`stdout`/`stderr` as an alias for `stdio`.
// Also normalize the `stdio` option.
export const normalizeStdioOption = ({stdio, ipc, buffer, ...options}, verboseInfo, isSync) => {
const stdioArray = getStdioArray(stdio, options).map((stdioOption, fdNumber) => addDefaultValue(stdioOption, fdNumber));
validateIpcStdioOption(stdioArray);
return isSync
? normalizeStdioSync(stdioArray, buffer, verboseInfo)
: normalizeIpcStdioArray(stdioArray, ipc);
};
// The `stdio: 'ipc'` value (raw `child_process` syntax) was replaced by the `ipc: true` option
const validateIpcStdioOption = stdioArray => {
if (stdioArray.some(stdioOption => hasIpcStdioOption(stdioOption))) {
throw new Error('The `ipc: true` option must be used instead of `stdio: \'ipc\'`.');
}
};
const hasIpcStdioOption = stdioOption => {
if (Array.isArray(stdioOption)) {
return stdioOption.some(item => hasIpcStdioItem(item));
}
return hasIpcStdioItem(stdioOption);
};
const hasIpcStdioItem = stdioOption => {
if (isStdioValueObject(stdioOption)) {
return stdioOption.value === 'ipc';
}
return stdioOption === 'ipc';
};
const getStdioArray = (stdio, options) => {
if (stdio === undefined) {
return STANDARD_STREAMS_ALIASES.map(alias => options[alias]);
}
if (hasAlias(options)) {
throw new Error(`It's not possible to provide \`stdio\` in combination with one of ${STANDARD_STREAMS_ALIASES.map(alias => `\`${alias}\``).join(', ')}`);
}
if (typeof stdio === 'string') {
return [stdio, stdio, stdio];
}
if (!Array.isArray(stdio)) {
throw new TypeError(`Expected \`stdio\` to be of type \`string\` or \`Array\`, got \`${typeof stdio}\``);
}
const length = Math.max(stdio.length, STANDARD_STREAMS_ALIASES.length);
return Array.from({length}, (_, fdNumber) => stdio[fdNumber]);
};
const hasAlias = options => STANDARD_STREAMS_ALIASES.some(alias => options[alias] !== undefined);
const addDefaultValue = (stdioOption, fdNumber) => {
if (Array.isArray(stdioOption)) {
return stdioOption.map(item => addDefaultValue(item, fdNumber));
}
if (stdioOption === null || stdioOption === undefined) {
return fdNumber >= STANDARD_STREAMS_ALIASES.length ? 'ignore' : 'pipe';
}
return stdioOption;
};
// Using `buffer: false` with synchronous methods implies `stdout`/`stderr`: `ignore`.
// Unless the output is needed, e.g. due to `verbose: 'full'` or to redirecting to a file.
const normalizeStdioSync = (stdioArray, buffer, verboseInfo) => stdioArray.map((stdioOption, fdNumber) =>
!buffer[fdNumber]
&& fdNumber !== 0
&& !isFullVerbose(verboseInfo, fdNumber)
&& isOutputPipeOnly(stdioOption, fdNumber)
? 'ignore'
: stdioOption);
const isOutputPipeOnly = (stdioOption, fdNumber) => isOutputPipe(stdioOption, fdNumber)
|| (Array.isArray(stdioOption) && stdioOption.every(item => isOutputPipe(item, fdNumber)));
const isOutputPipe = (stdioOption, fdNumber) => stdioOption === 'pipe'
|| isOutputPipeObject(stdioOption, fdNumber);
const isOutputPipeObject = (stdioOption, fdNumber) => isStdioValueObject(stdioOption)
&& stdioOption.value === 'pipe'
&& (stdioOption.input === undefined || stdioOption.input === false || isFixedOutputPipe(fdNumber, stdioOption.input));
// On `stdout`/`stderr`, the direction is always output, so `input: true` is ignored and the pipe stays output.
const isFixedOutputPipe = (fdNumber, input) => input === true && (fdNumber === 1 || fdNumber === 2);
+181
View File
@@ -0,0 +1,181 @@
import {isStream as isNodeStream, isDuplexStream} from 'is-stream';
import isPlainObj from 'is-plain-obj';
import {isUint8Array} from '../utils/uint-array.js';
// The `stdin`/`stdout`/`stderr` option can be of many types. This detects it.
export const getStdioItemType = (value, optionName) => {
if (isAsyncGenerator(value)) {
return 'asyncGenerator';
}
if (isSyncGenerator(value)) {
return 'generator';
}
if (isUrl(value)) {
return 'fileUrl';
}
if (isFilePathObject(value)) {
return 'filePath';
}
if (isWebStream(value)) {
return 'webStream';
}
if (isNodeStream(value, {checkOpen: false})) {
return 'native';
}
if (isUint8Array(value)) {
return 'uint8Array';
}
if (isAsyncIterableObject(value)) {
return 'asyncIterable';
}
if (isIterableObject(value)) {
return 'iterable';
}
if (isTransformStream(value)) {
return getTransformStreamType({transform: value}, optionName);
}
if (isTransformOptions(value)) {
return getTransformObjectType(value, optionName);
}
return 'native';
};
const getTransformObjectType = (value, optionName) => {
if (isDuplexStream(value.transform, {checkOpen: false})) {
return getDuplexType(value, optionName);
}
if (isTransformStream(value.transform)) {
return getTransformStreamType(value, optionName);
}
return getGeneratorObjectType(value, optionName);
};
const getDuplexType = (value, optionName) => {
validateNonGeneratorType(value, optionName, 'Duplex stream');
return 'duplex';
};
const getTransformStreamType = (value, optionName) => {
validateNonGeneratorType(value, optionName, 'web TransformStream');
return 'webTransform';
};
const validateNonGeneratorType = ({final, binary, objectMode}, optionName, typeName) => {
checkUndefinedOption(final, `${optionName}.final`, typeName);
checkUndefinedOption(binary, `${optionName}.binary`, typeName);
checkBooleanOption(objectMode, `${optionName}.objectMode`);
};
const checkUndefinedOption = (value, optionName, typeName) => {
if (value !== undefined) {
throw new TypeError(`The \`${optionName}\` option can only be defined when using a generator, not a ${typeName}.`);
}
};
const getGeneratorObjectType = ({transform, final, binary, objectMode}, optionName) => {
if (transform !== undefined && !isGenerator(transform)) {
throw new TypeError(`The \`${optionName}.transform\` option must be a generator, a Duplex stream or a web TransformStream.`);
}
if (isDuplexStream(final, {checkOpen: false})) {
throw new TypeError(`The \`${optionName}.final\` option must not be a Duplex stream.`);
}
if (isTransformStream(final)) {
throw new TypeError(`The \`${optionName}.final\` option must not be a web TransformStream.`);
}
if (final !== undefined && !isGenerator(final)) {
throw new TypeError(`The \`${optionName}.final\` option must be a generator.`);
}
checkBooleanOption(binary, `${optionName}.binary`);
checkBooleanOption(objectMode, `${optionName}.objectMode`);
return isAsyncGenerator(transform) || isAsyncGenerator(final) ? 'asyncGenerator' : 'generator';
};
export const checkBooleanOption = (value, optionName) => {
if (value !== undefined && typeof value !== 'boolean') {
throw new TypeError(`The \`${optionName}\` option must use a boolean.`);
}
};
const isGenerator = value => isAsyncGenerator(value) || isSyncGenerator(value);
export const isAsyncGenerator = value => Object.prototype.toString.call(value) === '[object AsyncGeneratorFunction]';
const isSyncGenerator = value => Object.prototype.toString.call(value) === '[object GeneratorFunction]';
const isTransformOptions = value => isPlainObj(value)
&& (value.transform !== undefined || value.final !== undefined);
export const isUrl = value => Object.prototype.toString.call(value) === '[object URL]';
export const isRegularUrl = value => isUrl(value) && value.protocol !== 'file:';
const isFilePathObject = value => isPlainObj(value)
&& Object.keys(value).length > 0
&& Object.keys(value).every(key => FILE_PATH_KEYS.has(key))
&& isFilePathString(value.file);
const FILE_PATH_KEYS = new Set(['file', 'append']);
export const isFilePathString = file => typeof file === 'string';
// The `{value, input}` object form marks an additional file descriptor's direction explicitly.
// This is needed for direction-ambiguous values (like `'pipe'`, `'inherit'`, a file or a transform) on fd3+, which otherwise default to output.
export const isStdioValueObject = value => isPlainObj(value)
&& Object.keys(value).length > 0
&& Object.keys(value).every(key => STDIO_VALUE_KEYS.has(key))
&& 'value' in value;
const STDIO_VALUE_KEYS = new Set(['value', 'input']);
export const isUnknownStdioString = (type, value) => type === 'native'
&& typeof value === 'string'
&& !KNOWN_STDIO_STRINGS.has(value);
const KNOWN_STDIO_STRINGS = new Set(['ipc', 'ignore', 'inherit', 'overlapped', 'pipe']);
const isReadableStream = value => Object.prototype.toString.call(value) === '[object ReadableStream]';
export const isWritableStream = value => Object.prototype.toString.call(value) === '[object WritableStream]';
const isWebStream = value => isReadableStream(value) || isWritableStream(value);
const isTransformStream = value => isReadableStream(value?.readable) && isWritableStream(value?.writable);
const isAsyncIterableObject = value => isObject(value) && typeof value[Symbol.asyncIterator] === 'function';
const isIterableObject = value => isObject(value) && typeof value[Symbol.iterator] === 'function';
const isObject = value => typeof value === 'object' && value !== null;
// Types which modify `subprocess.std*`
export const TRANSFORM_TYPES = new Set(['generator', 'asyncGenerator', 'duplex', 'webTransform']);
// Types which write to a file or a file descriptor
export const FILE_TYPES = new Set(['fileUrl', 'filePath', 'fileNumber']);
// When two file descriptors of this type share the same target, we need to do some special logic
export const SPECIAL_DUPLICATE_TYPES_SYNC = new Set(['fileUrl', 'filePath']);
export const SPECIAL_DUPLICATE_TYPES = new Set([...SPECIAL_DUPLICATE_TYPES_SYNC, 'webStream', 'nodeStream']);
// Do not allow two file descriptors of this type sharing the same target
export const FORBID_DUPLICATE_TYPES = new Set(['webTransform', 'duplex']);
// Convert types to human-friendly strings for error messages
export const TYPE_TO_MESSAGE = {
generator: 'a generator',
asyncGenerator: 'an async generator',
fileUrl: 'a file URL',
filePath: 'a file path string',
fileNumber: 'a file descriptor number',
webStream: 'a web stream',
nodeStream: 'a Node.js stream',
webTransform: 'a web TransformStream',
duplex: 'a Duplex stream',
native: 'any value',
iterable: 'an iterable',
asyncIterable: 'an async iterable',
string: 'a string',
uint8Array: 'a Uint8Array',
};