Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more code comments #984

Merged
merged 1 commit into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/arguments/command.js
Original file line number Diff line number Diff line change
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe standardize with . at the end? Some comments have it and some don't.

Copy link
Collaborator Author

@ehmicky ehmicky Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the comments do not use periods when there is a single sentence, and use periods otherwise.

Would you prefer to always using periods, even when there is a single sentence?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a big deal. Just felt a bit inconsistent.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
6 changes: 5 additions & 1 deletion lib/methods/main-async.js
Original file line number Diff line number Diff line change
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,7 +67,8 @@ const handleAsyncArguments = (rawFile, rawArguments, rawOptions) => {
}
};

// Prevent passing the `timeout` option directly to `child_process.spawn()`
// 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) {
throw new TypeError('The "signal" option has been renamed to "cancelSignal" instead.');
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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};