Skip to content

Commit

Permalink
Add more code comments
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Apr 23, 2024
1 parent bd3b478 commit d99f6ce
Show file tree
Hide file tree
Showing 60 changed files with 147 additions and 12 deletions.
1 change: 1 addition & 0 deletions lib/arguments/command.js
Expand Up @@ -4,6 +4,7 @@ import {getStartTime} from '../return/duration.js';
import {joinCommand} from './escape.js';
import {normalizeFdSpecificOption} from './specific.js';

// Compute `result.command`, `result.escapedCommand` and `verbose`-related information
export const handleCommand = (filePath, rawArguments, rawOptions) => {
const startTime = getStartTime();
const {command, escapedCommand} = joinCommand(filePath, rawArguments);
Expand Down
2 changes: 2 additions & 0 deletions lib/arguments/cwd.js
Expand Up @@ -3,6 +3,7 @@ import {resolve} from 'node:path';
import process from 'node:process';
import {safeNormalizeFileUrl} from './file-url.js';

// Normalize `cwd` option
export const normalizeCwd = (cwd = getDefaultCwd()) => {
const cwdString = safeNormalizeFileUrl(cwd, 'The "cwd" option');
return resolve(cwdString);
Expand All @@ -17,6 +18,7 @@ const getDefaultCwd = () => {
}
};

// When `cwd` option has an invalid value, provide with a better error message
export const fixCwdError = (originalMessage, cwd) => {
if (cwd === getDefaultCwd()) {
return originalMessage;
Expand Down
1 change: 1 addition & 0 deletions lib/arguments/encoding-option.js
@@ -1,3 +1,4 @@
// Validate `encoding` option
export const validateEncoding = ({encoding}) => {
if (ENCODINGS.has(encoding)) {
return;
Expand Down
2 changes: 2 additions & 0 deletions lib/arguments/escape.js
@@ -1,6 +1,7 @@
import {platform} from 'node:process';
import {stripVTControlCharacters} from 'node:util';

// Compute `result.command` and `result.escapedCommand`
export const joinCommand = (filePath, rawArguments) => {
const fileAndArguments = [filePath, ...rawArguments];
const command = fileAndArguments.join(' ');
Expand All @@ -10,6 +11,7 @@ export const joinCommand = (filePath, rawArguments) => {
return {command, escapedCommand};
};

// Remove ANSI sequences and escape control characters and newlines
export const escapeLines = lines => stripVTControlCharacters(lines)
.split('\n')
.map(line => escapeControlCharacters(line))
Expand Down
3 changes: 3 additions & 0 deletions lib/arguments/fd-options.js
@@ -1,5 +1,6 @@
import {parseFd} from './specific.js';

// Retrieve stream targeted by the `to` option
export const getToStream = (destination, to = 'stdin') => {
const isWritable = true;
const {options, fileDescriptors} = SUBPROCESS_OPTIONS.get(destination);
Expand All @@ -13,6 +14,7 @@ export const getToStream = (destination, to = 'stdin') => {
return destinationStream;
};

// Retrieve stream targeted by the `from` option
export const getFromStream = (source, from = 'stdout') => {
const isWritable = false;
const {options, fileDescriptors} = SUBPROCESS_OPTIONS.get(source);
Expand All @@ -26,6 +28,7 @@ export const getFromStream = (source, from = 'stdout') => {
return sourceStream;
};

// Keeps track of the options passed to each Execa call
export const SUBPROCESS_OPTIONS = new WeakMap();

const getFdNumber = (fileDescriptors, fdName, isWritable) => {
Expand Down
2 changes: 2 additions & 0 deletions lib/arguments/file-url.js
@@ -1,5 +1,6 @@
import {fileURLToPath} from 'node:url';

// Allow some arguments/options to be either a file path string or a file URL
export const safeNormalizeFileUrl = (file, name) => {
const fileString = normalizeFileUrl(file);

Expand All @@ -10,4 +11,5 @@ export const safeNormalizeFileUrl = (file, name) => {
return fileString;
};

// Same but also allows other values, e.g. `boolean` for the `shell` option
export const normalizeFileUrl = file => file instanceof URL ? fileURLToPath(file) : file;
2 changes: 2 additions & 0 deletions lib/arguments/options.js
Expand Up @@ -10,6 +10,8 @@ import {normalizeCwd} from './cwd.js';
import {normalizeFileUrl} from './file-url.js';
import {normalizeFdSpecificOptions} from './specific.js';

// Normalize the options object, and sometimes also the file paths and arguments.
// Applies default values, validate allowed options, normalize them.
export const normalizeOptions = (filePath, rawArguments, rawOptions) => {
rawOptions.cwd = normalizeCwd(rawOptions.cwd);
const [processedFile, processedArguments, processedOptions] = handleNodeOption(filePath, rawArguments, rawOptions);
Expand Down
5 changes: 5 additions & 0 deletions lib/arguments/specific.js
Expand Up @@ -2,6 +2,9 @@ import isPlainObject from 'is-plain-obj';
import {STANDARD_STREAMS_ALIASES} from '../utils/standard-stream.js';
import {verboseDefault} from '../verbose/info.js';

// Some options can have different values for `stdout`/`stderr`/`fd3`.
// This normalizes those to array of values.
// For example, `{verbose: {stdout: 'none', stderr: 'full'}}` becomes `{verbose: ['none', 'none', 'full']}`
export const normalizeFdSpecificOptions = options => {
const optionsCopy = {...options};

Expand Down Expand Up @@ -62,6 +65,7 @@ Please set the "stdio" option to ensure that file descriptor exists.`);
return fdNumber === 'all' ? [1, 2] : [fdNumber];
};

// Use the same syntax for fd-specific options and the `from`/`to` options
export const parseFd = fdName => {
if (fdName === 'all') {
return fdName;
Expand Down Expand Up @@ -91,4 +95,5 @@ const DEFAULT_OPTIONS = {
stripFinalNewline: true,
};

// List of options which can have different values for `stdout`/`stderr`.
export const FD_SPECIFIC_OPTIONS = ['lines', 'buffer', 'maxBuffer', 'verbose', 'stripFinalNewline'];
1 change: 1 addition & 0 deletions lib/convert/add.js
Expand Up @@ -4,6 +4,7 @@ import {createWritable} from './writable.js';
import {createDuplex} from './duplex.js';
import {createIterable} from './iterable.js';

// Add methods to convert the subprocess to a stream or iterable
export const addConvertedStreams = (subprocess, {encoding}) => {
const concurrentStreams = initializeConcurrentStreams();
subprocess.readable = createReadable.bind(undefined, {subprocess, concurrentStreams, encoding});
Expand Down
2 changes: 1 addition & 1 deletion lib/convert/duplex.js
Expand Up @@ -15,7 +15,7 @@ import {
onWritableDestroy,
} from './writable.js';

// Create a `Duplex` stream combining both
// Create a `Duplex` stream combining both `subprocess.readable()` and `subprocess.writable()`
export const createDuplex = ({subprocess, concurrentStreams, encoding}, {from, to, binary: binaryOption = true, preserveNewlines = true} = {}) => {
const binary = binaryOption || BINARY_ENCODINGS.has(encoding);
const {subprocessStdout, waitReadableDestroy} = getSubprocessStdout(subprocess, from, concurrentStreams);
Expand Down
1 change: 1 addition & 0 deletions lib/convert/iterable.js
Expand Up @@ -2,6 +2,7 @@ import {BINARY_ENCODINGS} from '../arguments/encoding-option.js';
import {getFromStream} from '../arguments/fd-options.js';
import {iterateOnSubprocessStream} from '../io/iterate.js';

// Convert the subprocess to an async iterable
export const createIterable = (subprocess, encoding, {
from,
binary: binaryOption = false,
Expand Down
2 changes: 2 additions & 0 deletions lib/io/contents.js
Expand Up @@ -6,6 +6,7 @@ import {iterateForResult} from './iterate.js';
import {handleMaxBuffer} from './max-buffer.js';
import {getStripFinalNewline} from './strip-newline.js';

// Retrieve `result.stdout|stderr|all|stdio[*]`
export const getStreamOutput = async ({stream, onStreamEnd, fdNumber, encoding, buffer, maxBuffer, lines, allMixed, stripFinalNewline, verboseInfo, streamInfo: {fileDescriptors}}) => {
if (shouldLogOutput({
stdioItems: fileDescriptors[fdNumber]?.stdioItems,
Expand Down Expand Up @@ -91,6 +92,7 @@ export const getBufferedData = async streamPromise => {
}
};

// Ensure we are returning Uint8Arrays when using `encoding: 'buffer'`
const handleBufferedData = ({bufferedData}) => isArrayBuffer(bufferedData)
? new Uint8Array(bufferedData)
: bufferedData;
6 changes: 6 additions & 0 deletions lib/io/max-buffer.js
@@ -1,6 +1,8 @@
import {MaxBufferError} from 'get-stream';
import {getStreamName} from '../utils/standard-stream.js';

// When the `maxBuffer` option is hit, a MaxBufferError is thrown.
// The stream is aborted, then specific information is kept for the error message.
export const handleMaxBuffer = ({error, stream, readableObjectMode, lines, encoding, fdNumber}) => {
if (!(error instanceof MaxBufferError)) {
throw error;
Expand Down Expand Up @@ -32,6 +34,7 @@ const getMaxBufferUnit = (readableObjectMode, lines, encoding) => {
return 'characters';
};

// Error message when `maxBuffer` is hit
export const getMaxBufferMessage = (error, maxBuffer) => {
const {streamName, threshold, unit} = getMaxBufferInfo(error, maxBuffer);
return `Command's ${streamName} was larger than ${threshold} ${unit}`;
Expand All @@ -47,6 +50,9 @@ const getMaxBufferInfo = (error, maxBuffer) => {
return {streamName: getStreamName(fdNumber), threshold: maxBuffer[fdNumber], unit};
};

// The only way to apply `maxBuffer` with `spawnSync()` is to use the native `maxBuffer` option Node.js provides.
// However, this has multiple limitations, and cannot behave the exact same way as the async behavior.
// When the `maxBuffer` is hit, a `ENOBUFS` error is thrown.
export const isMaxBufferSync = (resultError, output, maxBuffer) => resultError?.code === 'ENOBUFS'
&& output !== null
&& output.some(result => result !== null && result.length > getMaxBufferSync(maxBuffer));
Expand Down
4 changes: 3 additions & 1 deletion lib/io/output-async.js
Expand Up @@ -32,7 +32,7 @@ export const pipeOutputAsync = (subprocess, fileDescriptors, controller) => {
}
};

// `subprocess.stdin|stdout|stderr|stdio` is directly mutated.
// When using transforms, `subprocess.stdin|stdout|stderr|stdio` is directly mutated
const pipeTransform = (subprocess, stream, direction, fdNumber) => {
if (direction === 'output') {
pipeStreams(subprocess.stdio[fdNumber], stream);
Expand All @@ -50,6 +50,8 @@ const pipeTransform = (subprocess, stream, direction, fdNumber) => {

const SUBPROCESS_STREAM_PROPERTIES = ['stdin', 'stdout', 'stderr'];

// Most `std*` option values involve piping `subprocess.std*` to a stream.
// The stream is either passed by the user or created internally.
const pipeStdioItem = ({subprocess, stream, direction, fdNumber, inputStreamsGroups, controller}) => {
if (stream === undefined) {
return;
Expand Down
5 changes: 5 additions & 0 deletions lib/io/output-sync.js
Expand Up @@ -67,6 +67,7 @@ const transformOutputResultSync = ({result, fileDescriptors, fdNumber, state, is
}
};

// Applies transform generators to `stdout`/`stderr`
const runOutputGeneratorsSync = (chunks, stdioItems, encoding, state) => {
try {
return runGeneratorsSync(chunks, stdioItems, encoding, false);
Expand All @@ -76,6 +77,9 @@ const runOutputGeneratorsSync = (chunks, stdioItems, encoding, state) => {
}
};

// The contents is converted to three stages:
// - serializedResult: used when the target is a file path/URL or a file descriptor (including 'inherit')
// - finalResult/returnedResult: returned as `result.std*`
const serializeChunks = ({chunks, objectMode, encoding, lines, stripFinalNewline, fdNumber}) => {
if (objectMode) {
return {serializedResult: chunks};
Expand All @@ -93,6 +97,7 @@ const serializeChunks = ({chunks, objectMode, encoding, lines, stripFinalNewline
return {serializedResult};
};

// When the `std*` target is a file path/URL or a file descriptor
const writeToFiles = (serializedResult, stdioItems) => {
for (const {type, path} of stdioItems) {
if (FILE_TYPES.has(type)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/io/pipeline.js
@@ -1,7 +1,7 @@
import {finished} from 'node:stream/promises';
import {isStandardStream} from '../utils/standard-stream.js';

// Like `Stream.pipeline(source, destination)`, but does not destroy standard streams.
// Similar to `Stream.pipeline(source, destination)`, but does not destroy standard streams
export const pipeStreams = (source, destination) => {
source.pipe(destination);
onSourceFinish(source, destination);
Expand Down
3 changes: 3 additions & 0 deletions lib/io/strip-newline.js
@@ -1,9 +1,12 @@
import stripFinalNewlineFunction from 'strip-final-newline';

// Apply `stripFinalNewline` option, which applies to `result.stdout|stderr|all|stdio[*]`.
// If the `lines` option is used, it is applied on each line, but using a different function.
export const stripNewline = (value, {stripFinalNewline}, fdNumber) => getStripFinalNewline(stripFinalNewline, fdNumber) && value !== undefined && !Array.isArray(value)
? stripFinalNewlineFunction(value)
: value;

// Retrieve `stripFinalNewline` option value, including with `subprocess.all`
export const getStripFinalNewline = (stripFinalNewline, fdNumber) => fdNumber === 'all'
? stripFinalNewline[1] || stripFinalNewline[2]
: stripFinalNewline[fdNumber];
4 changes: 4 additions & 0 deletions lib/methods/command.js
@@ -1,6 +1,10 @@
// Main logic for `execaCommand()`
export const mapCommandAsync = ({file, commandArguments}) => parseCommand(file, commandArguments);

// Main logic for `execaCommandSync()`
export const mapCommandSync = ({file, commandArguments}) => ({...parseCommand(file, commandArguments), isSync: true});

// Convert `execaCommand(command)` into `execa(file, ...commandArguments)`
const parseCommand = (command, unusedArguments) => {
if (unusedArguments.length > 0) {
throw new TypeError(`The command and its arguments must be passed as a single string: ${command} ${unusedArguments}.`);
Expand Down
5 changes: 5 additions & 0 deletions lib/methods/create.js
Expand Up @@ -5,6 +5,11 @@ import {execaCoreSync} from './main-sync.js';
import {execaCoreAsync} from './main-async.js';
import {mergeOptions} from './bind.js';

// Wraps every exported methods to provide the following features:
// - template string syntax: execa`command argument`
// - options binding: boundExeca = execa(options)
// - optional argument/options: execa(file), execa(file, args), execa(file, options), execa(file, args, options)
// `mapArguments()` and `setBoundExeca()` allows for method-specific logic.
export const createExeca = (mapArguments, boundOptions, deepOptions, setBoundExeca) => {
const createNested = (mapArguments, boundOptions, setBoundExeca) => createExeca(mapArguments, boundOptions, deepOptions, setBoundExeca);
const boundExeca = (...execaArguments) => callBoundExeca({
Expand Down
4 changes: 4 additions & 0 deletions lib/methods/main-async.js
Expand Up @@ -19,6 +19,7 @@ import {waitForSubprocessResult} from '../resolve/wait-subprocess.js';
import {addConvertedStreams} from '../convert/add.js';
import {mergePromise} from './promise.js';

// Main shared logic for all async methods: `execa()`, `execaCommand()`, `$`, `execaNode()`
export const execaCoreAsync = (rawFile, rawArguments, rawOptions, createNested) => {
const {file, commandArguments, command, escapedCommand, startTime, verboseInfo, options, fileDescriptors} = handleAsyncArguments(rawFile, rawArguments, rawOptions);
const {subprocess, promise} = spawnSubprocessAsync({
Expand All @@ -42,6 +43,7 @@ export const execaCoreAsync = (rawFile, rawArguments, rawOptions, createNested)
return subprocess;
};

// Compute arguments to pass to `child_process.spawn()`
const handleAsyncArguments = (rawFile, rawArguments, rawOptions) => {
const {command, escapedCommand, startTime, verboseInfo} = handleCommand(rawFile, rawArguments, rawOptions);

Expand All @@ -65,6 +67,7 @@ const handleAsyncArguments = (rawFile, rawArguments, rawOptions) => {
}
};

// Options normalization logic specific to async methods.
// Prevent passing the `timeout` option directly to `child_process.spawn()`
const handleAsyncOptions = ({timeout, signal, cancelSignal, ...options}) => {
if (signal !== undefined) {
Expand Down Expand Up @@ -120,6 +123,7 @@ const spawnSubprocessAsync = ({file, commandArguments, options, startTime, verbo
return {subprocess, promise};
};

// Asynchronous logic, as opposed to the previous logic which can be run synchronously, i.e. can be returned to user right away
const handlePromise = async ({subprocess, options, startTime, verboseInfo, fileDescriptors, originalStreams, command, escapedCommand, controller}) => {
const context = {timedOut: false};

Expand Down
5 changes: 5 additions & 0 deletions lib/methods/main-sync.js
Expand Up @@ -12,6 +12,7 @@ import {logEarlyResult} from '../verbose/complete.js';
import {getAllSync} from '../resolve/all-sync.js';
import {getExitResultSync} from '../resolve/exit-sync.js';

// Main shared logic for all sync methods: `execaSync()`, `execaCommandSync()`, `$.sync()`
export const execaCoreSync = (rawFile, rawArguments, rawOptions) => {
const {file, commandArguments, command, escapedCommand, startTime, verboseInfo, options, fileDescriptors} = handleSyncArguments(rawFile, rawArguments, rawOptions);
const result = spawnSubprocessSync({
Expand All @@ -27,6 +28,7 @@ export const execaCoreSync = (rawFile, rawArguments, rawOptions) => {
return handleResult(result, verboseInfo, options);
};

// Compute arguments to pass to `child_process.spawnSync()`
const handleSyncArguments = (rawFile, rawArguments, rawOptions) => {
const {command, escapedCommand, startTime, verboseInfo} = handleCommand(rawFile, rawArguments, rawOptions);

Expand All @@ -51,8 +53,10 @@ const handleSyncArguments = (rawFile, rawArguments, rawOptions) => {
}
};

// Options normalization logic specific to sync methods
const normalizeSyncOptions = options => options.node && !options.ipc ? {...options, ipc: false} : options;

// Options validation logic specific to sync methods
const validateSyncOptions = ({ipc, detached, cancelSignal}) => {
if (ipc) {
throwInvalidSyncOption('ipc: true');
Expand Down Expand Up @@ -128,6 +132,7 @@ const runSubprocessSync = ({file, commandArguments, options, command, escapedCom
}
};

// The `encoding` option is handled by Execa, not by `child_process.spawnSync()`.
const normalizeSpawnSyncOptions = ({encoding, maxBuffer, ...options}) => ({...options, encoding: 'buffer', maxBuffer: getMaxBufferSync(maxBuffer)});

const getSyncResult = ({error, exitCode, signal, timedOut, isMaxBuffer, stdio, all, options, command, escapedCommand, startTime}) => error === undefined
Expand Down
4 changes: 4 additions & 0 deletions lib/methods/node.js
Expand Up @@ -2,6 +2,7 @@ import {execPath, execArgv} from 'node:process';
import {basename, resolve} from 'node:path';
import {safeNormalizeFileUrl} from '../arguments/file-url.js';

// `execaNode()` is a shortcut for `execa(..., {node: true})`
export const mapNode = ({options}) => {
if (options.node === false) {
throw new TypeError('The "node" option cannot be false with `execaNode()`.');
Expand All @@ -10,6 +11,9 @@ export const mapNode = ({options}) => {
return {options: {...options, node: true}};
};

// Applies the `node: true` option, and the related `nodePath`/`nodeOptions` options
// Modifies the file commands/arguments to ensure the same Node binary and flags are re-used.
// Also adds `ipc: true` and `shell: false`.
export const handleNodeOption = (file, commandArguments, {
node: shouldHandleNode = false,
nodePath = execPath,
Expand Down
2 changes: 2 additions & 0 deletions lib/methods/parameters.js
@@ -1,6 +1,8 @@
import isPlainObject from 'is-plain-obj';
import {safeNormalizeFileUrl} from '../arguments/file-url.js';

// The command `arguments` and `options` are both optional.
// This also does basic validation on them and on the command file.
export const normalizeParameters = (rawFile, rawArguments = [], rawOptions = {}) => {
const filePath = safeNormalizeFileUrl(rawFile, 'First argument');
const [commandArguments, options] = isPlainObject(rawArguments)
Expand Down
7 changes: 7 additions & 0 deletions lib/methods/script.js
@@ -1,15 +1,22 @@
// Sets `$.sync` and `$.s`
export const setScriptSync = (boundExeca, createNested, boundOptions) => {
boundExeca.sync = createNested(mapScriptSync, boundOptions);
boundExeca.s = boundExeca.sync;
};

// Main logic for `$`
export const mapScriptAsync = ({options}) => getScriptOptions(options);

// Main logic for `$.sync`
const mapScriptSync = ({options}) => ({...getScriptOptions(options), isSync: true});

// `$` is like `execa` but with script-friendly options: `{stdin: 'inherit', preferLocal: true}`
const getScriptOptions = options => ({options: {...getScriptStdinOption(options), ...options}});

const getScriptStdinOption = ({input, inputFile, stdio}) => input === undefined && inputFile === undefined && stdio === undefined
? {stdin: 'inherit'}
: {};

// When using $(...).pipe(...), most script-friendly options should apply to both commands.
// However, some options (like `stdin: 'inherit'`) would create issues with piping, i.e. cannot be deep.
export const deepScriptOptions = {preferLocal: true};

0 comments on commit d99f6ce

Please sign in to comment.