From 924c488b1b5c1d7363598a4429445e145b908e36 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 1 Jul 2026 19:32:32 +0200 Subject: [PATCH] Fix fd-specific IPC aliasing Keep numeric fd-specific options separate from the internal IPC slot by validating fd names against the real stdio length while reserving a distinct pseudo-slot for ipc. Update result types so ipc-specific options do not affect numeric fd lookups, and add runtime and tsd regressions for maxBuffer, buffer, verbose, and result stdio typing. Fixes #1239 --- lib/arguments/specific.js | 19 ++++++------ test-d/arguments/specific.test-d.ts | 20 ++++++------ test-d/return/no-buffer-specific.test-d.ts | 7 +++++ test/helpers/verbose.js | 11 +++++-- test/io/max-buffer.js | 20 ++++++++++++ test/ipc/buffer-messages.js | 13 ++++++++ test/verbose/complete.js | 13 ++++---- test/verbose/error.js | 11 ++++--- test/verbose/ipc.js | 20 ++++++++++-- test/verbose/output-enable.js | 5 +-- test/verbose/start.js | 16 +++++++--- types/arguments/specific.d.ts | 36 ++++++++++++---------- 12 files changed, 135 insertions(+), 56 deletions(-) diff --git a/lib/arguments/specific.js b/lib/arguments/specific.js index 1238c0df50..fe8e1e32a3 100644 --- a/lib/arguments/specific.js +++ b/lib/arguments/specific.js @@ -16,8 +16,9 @@ export const normalizeFdSpecificOptions = options => { }; export const normalizeFdSpecificOption = (options, optionName) => { - const optionBaseArray = Array.from({length: getStdioLength(options) + 1}); - const optionArray = normalizeFdSpecificValue(options[optionName], optionBaseArray, optionName); + const stdioLength = getStdioLength(options); + const optionBaseArray = Array.from({length: stdioLength + 1}); + const optionArray = normalizeFdSpecificValue(options[optionName], optionBaseArray, optionName, stdioLength); return addDefaultValue(optionArray, optionName); }; @@ -25,13 +26,13 @@ const getStdioLength = ({stdio}) => Array.isArray(stdio) ? Math.max(stdio.length, STANDARD_STREAMS_ALIASES.length) : STANDARD_STREAMS_ALIASES.length; -const normalizeFdSpecificValue = (optionValue, optionArray, optionName) => isPlainObject(optionValue) - ? normalizeOptionObject(optionValue, optionArray, optionName) +const normalizeFdSpecificValue = (optionValue, optionArray, optionName, stdioLength) => isPlainObject(optionValue) + ? normalizeOptionObject(optionValue, optionArray, optionName, stdioLength) : optionArray.fill(optionValue); -const normalizeOptionObject = (optionValue, optionArray, optionName) => { +const normalizeOptionObject = (optionValue, optionArray, optionName, stdioLength) => { for (const fdName of Object.keys(optionValue).sort(compareFdName)) { - for (const fdNumber of parseFdName(fdName, optionName, optionArray)) { + for (const fdNumber of parseFdName(fdName, optionName, stdioLength)) { optionArray[fdNumber] = optionValue[fdName]; } } @@ -50,9 +51,9 @@ const getFdNameOrder = fdName => { return fdName === 'all' ? 2 : 1; }; -const parseFdName = (fdName, optionName, optionArray) => { +const parseFdName = (fdName, optionName, stdioLength) => { if (fdName === 'ipc') { - return [optionArray.length - 1]; + return [stdioLength]; } const fdNumber = parseFd(fdName); @@ -61,7 +62,7 @@ const parseFdName = (fdName, optionName, optionArray) => { It must be "${optionName}.stdout", "${optionName}.stderr", "${optionName}.all", "${optionName}.ipc", or "${optionName}.fd3", "${optionName}.fd4" (and so on).`); } - if (fdNumber >= optionArray.length) { + if (fdNumber !== 'all' && fdNumber >= stdioLength) { throw new TypeError(`"${optionName}.${fdName}" is invalid: that file descriptor does not exist. Please set the "stdio" option to ensure that file descriptor exists.`); } diff --git a/test-d/arguments/specific.test-d.ts b/test-d/arguments/specific.test-d.ts index fd47e7f2e1..aafff607b4 100644 --- a/test-d/arguments/specific.test-d.ts +++ b/test-d/arguments/specific.test-d.ts @@ -9,7 +9,7 @@ await execa('unicorns', {maxBuffer: {stdout: 0, stderr: 0} as const}); await execa('unicorns', {maxBuffer: {all: 0}}); await execa('unicorns', {maxBuffer: {fd1: 0}}); await execa('unicorns', {maxBuffer: {fd2: 0}}); -await execa('unicorns', {maxBuffer: {fd3: 0}}); +await execa('unicorns', {maxBuffer: {fd3: 0}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); await execa('unicorns', {maxBuffer: {ipc: 0}}); expectError(await execa('unicorns', {maxBuffer: {stdout: '0'}})); @@ -21,7 +21,7 @@ execaSync('unicorns', {maxBuffer: {stdout: 0, stderr: 0} as const}); execaSync('unicorns', {maxBuffer: {all: 0}}); execaSync('unicorns', {maxBuffer: {fd1: 0}}); execaSync('unicorns', {maxBuffer: {fd2: 0}}); -execaSync('unicorns', {maxBuffer: {fd3: 0}}); +execaSync('unicorns', {maxBuffer: {fd3: 0}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); execaSync('unicorns', {maxBuffer: {ipc: 0}}); expectError(execaSync('unicorns', {maxBuffer: {stdout: '0'}})); @@ -33,7 +33,7 @@ await execa('unicorns', {verbose: {stdout: 'none', stderr: 'none'} as const}); await execa('unicorns', {verbose: {all: 'none'}}); await execa('unicorns', {verbose: {fd1: 'none'}}); await execa('unicorns', {verbose: {fd2: 'none'}}); -await execa('unicorns', {verbose: {fd3: 'none'}}); +await execa('unicorns', {verbose: {fd3: 'none'}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); await execa('unicorns', {verbose: {ipc: 'none'}}); expectError(await execa('unicorns', {verbose: {stdout: 'other'}})); @@ -45,7 +45,7 @@ execaSync('unicorns', {verbose: {stdout: 'none', stderr: 'none'} as const}); execaSync('unicorns', {verbose: {all: 'none'}}); execaSync('unicorns', {verbose: {fd1: 'none'}}); execaSync('unicorns', {verbose: {fd2: 'none'}}); -execaSync('unicorns', {verbose: {fd3: 'none'}}); +execaSync('unicorns', {verbose: {fd3: 'none'}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); execaSync('unicorns', {verbose: {ipc: 'none'}}); expectError(execaSync('unicorns', {verbose: {stdout: 'other'}})); @@ -57,7 +57,7 @@ await execa('unicorns', {stripFinalNewline: {stdout: true, stderr: true} as cons await execa('unicorns', {stripFinalNewline: {all: true}}); await execa('unicorns', {stripFinalNewline: {fd1: true}}); await execa('unicorns', {stripFinalNewline: {fd2: true}}); -await execa('unicorns', {stripFinalNewline: {fd3: true}}); +await execa('unicorns', {stripFinalNewline: {fd3: true}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); await execa('unicorns', {stripFinalNewline: {ipc: true}}); expectError(await execa('unicorns', {stripFinalNewline: {stdout: 'true'}})); @@ -69,7 +69,7 @@ execaSync('unicorns', {stripFinalNewline: {stdout: true, stderr: true} as const} execaSync('unicorns', {stripFinalNewline: {all: true}}); execaSync('unicorns', {stripFinalNewline: {fd1: true}}); execaSync('unicorns', {stripFinalNewline: {fd2: true}}); -execaSync('unicorns', {stripFinalNewline: {fd3: true}}); +execaSync('unicorns', {stripFinalNewline: {fd3: true}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); execaSync('unicorns', {stripFinalNewline: {ipc: true}}); expectError(execaSync('unicorns', {stripFinalNewline: {stdout: 'true'}})); @@ -81,7 +81,7 @@ await execa('unicorns', {lines: {stdout: true, stderr: true} as const}); await execa('unicorns', {lines: {all: true}}); await execa('unicorns', {lines: {fd1: true}}); await execa('unicorns', {lines: {fd2: true}}); -await execa('unicorns', {lines: {fd3: true}}); +await execa('unicorns', {lines: {fd3: true}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); await execa('unicorns', {lines: {ipc: true}}); expectError(await execa('unicorns', {lines: {stdout: 'true'}})); @@ -93,7 +93,7 @@ execaSync('unicorns', {lines: {stdout: true, stderr: true} as const}); execaSync('unicorns', {lines: {all: true}}); execaSync('unicorns', {lines: {fd1: true}}); execaSync('unicorns', {lines: {fd2: true}}); -execaSync('unicorns', {lines: {fd3: true}}); +execaSync('unicorns', {lines: {fd3: true}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); execaSync('unicorns', {lines: {ipc: true}}); expectError(execaSync('unicorns', {lines: {stdout: 'true'}})); @@ -105,7 +105,7 @@ await execa('unicorns', {buffer: {stdout: true, stderr: true} as const}); await execa('unicorns', {buffer: {all: true}}); await execa('unicorns', {buffer: {fd1: true}}); await execa('unicorns', {buffer: {fd2: true}}); -await execa('unicorns', {buffer: {fd3: true}}); +await execa('unicorns', {buffer: {fd3: true}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); await execa('unicorns', {buffer: {ipc: true}}); expectError(await execa('unicorns', {buffer: {stdout: 'true'}})); @@ -117,7 +117,7 @@ execaSync('unicorns', {buffer: {stdout: true, stderr: true} as const}); execaSync('unicorns', {buffer: {all: true}}); execaSync('unicorns', {buffer: {fd1: true}}); execaSync('unicorns', {buffer: {fd2: true}}); -execaSync('unicorns', {buffer: {fd3: true}}); +execaSync('unicorns', {buffer: {fd3: true}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); execaSync('unicorns', {buffer: {ipc: true}}); expectError(execaSync('unicorns', {buffer: {stdout: 'true'}})); diff --git a/test-d/return/no-buffer-specific.test-d.ts b/test-d/return/no-buffer-specific.test-d.ts index 136ca17fe9..22b8102121 100644 --- a/test-d/return/no-buffer-specific.test-d.ts +++ b/test-d/return/no-buffer-specific.test-d.ts @@ -45,6 +45,10 @@ expectType(noBufferFd3Result.all); expectType(noBufferFd3Result.stdio[3]); expectType(noBufferFd3Result.stdio[4]); +const noBufferIpcResult = await execa('unicorns', {buffer: {ipc: false}, ipc: true, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); +expectType(noBufferIpcResult.stdio[3]); +expectType<[]>(noBufferIpcResult.ipcOutput); + const noBufferStdoutResultSync = execaSync('unicorns', {all: true, buffer: {stdout: false}}); expectType(noBufferStdoutResultSync.stdout); expectType(noBufferStdoutResultSync.stdio[1]); @@ -88,3 +92,6 @@ expectType(noBufferFd3ResultSync.stdio[2]); expectType(noBufferFd3ResultSync.all); expectType(noBufferFd3ResultSync.stdio[3]); expectType(noBufferFd3ResultSync.stdio[4]); + +const noBufferIpcStdioResultSync = execaSync('unicorns', {buffer: {ipc: false}, stdio: ['pipe', 'pipe', 'pipe', 'pipe']}); +expectType(noBufferIpcStdioResultSync.stdio[3]); diff --git a/test/helpers/verbose.js b/test/helpers/verbose.js index 104fac26ee..d334dcc8ee 100644 --- a/test/helpers/verbose.js +++ b/test/helpers/verbose.js @@ -3,12 +3,14 @@ import {stripVTControlCharacters} from 'node:util'; import {replaceSymbols} from 'figures'; import {foobarString} from './input.js'; import {nestedSubprocess} from './nested.js'; +import {fullStdio} from './stdio.js'; const isWindows = platform === 'win32'; export const QUOTE = isWindows ? '"' : '\''; -export const runErrorSubprocess = async (t, verbose, isSync = false, expectExitCode = true) => { - const {stderr, nestedResult} = await nestedSubprocess('noop-fail.js', ['1', foobarString], {verbose, isSync}); +export const runErrorSubprocess = async (t, verbose, isSync = false, options = {}) => { + const {expectExitCode = true, ...subprocessOptions} = options; + const {stderr, nestedResult} = await nestedSubprocess('noop-fail.js', ['1', foobarString], {verbose, isSync, ...subprocessOptions}); t.true(nestedResult instanceof Error); if (expectExitCode) { t.true(stderr.includes('exit code 2')); @@ -44,6 +46,7 @@ export const runVerboseSubprocess = ({ ...options }) => nestedSubprocess('noop-verbose.js', [output], { ipc: !isSync, + ...getStdioForFd3Option(fdNumber, secondFdNumber), optionsFixture, optionsInput: { type, @@ -57,6 +60,10 @@ export const runVerboseSubprocess = ({ ...options, }); +export const getStdioForFd3Option = (...values) => values.some(value => hasFd3Option(value)) ? fullStdio : {}; + +const hasFd3Option = value => value === 'fd3' || (typeof value === 'object' && value !== null && Object.hasOwn(value, 'fd3')); + export const getCommandLine = stderr => getCommandLines(stderr)[0]; export const getCommandLines = stderr => getNormalizedLines(stderr).filter(line => isCommandLine(line)); const isCommandLine = line => line.includes(' $ ') || line.includes(' | '); diff --git a/test/io/max-buffer.js b/test/io/max-buffer.js index f29ec515b8..737881d3b9 100644 --- a/test/io/max-buffer.js +++ b/test/io/max-buffer.js @@ -129,6 +129,26 @@ test('maxBuffer does not affect other file descriptors with fd-specific options' t.false(isMaxBuffer); }); +test('maxBuffer.fd3 is invalid without stdio[3], even with ipc', t => { + const {message} = t.throws(() => { + execa('ipc-send-twice.js', {ipc: true, maxBuffer: {fd3: 1}}); + }); + t.true(message.includes('"maxBuffer.fd3" is invalid: that file descriptor does not exist.')); +}); + +test('maxBuffer.fd3 and maxBuffer.ipc are distinct', async t => { + const {isMaxBuffer, ipcOutput} = await execa('ipc-send-twice.js', { + ipc: true, + stdio: ['ignore', 'pipe', 'pipe', 'pipe'], + maxBuffer: { + fd3: 1, + ipc: 2, + }, + }); + t.false(isMaxBuffer); + t.deepEqual(ipcOutput, foobarArray); +}); + test('maxBuffer.stdout is used for other file descriptors with fd-specific options, sync', async t => { const length = 1; const {shortMessage, stderr} = await runMaxBuffer(t, execaSync, 2, {maxBuffer: {stdout: length}}); diff --git a/test/ipc/buffer-messages.js b/test/ipc/buffer-messages.js index fb22120573..28b87525b4 100644 --- a/test/ipc/buffer-messages.js +++ b/test/ipc/buffer-messages.js @@ -3,6 +3,7 @@ import {execa, execaSync} from '../../index.js'; import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; import {foobarString, foobarArray} from '../helpers/input.js'; import {PARALLEL_COUNT} from '../helpers/parallel.js'; +import {fullStdio} from '../helpers/stdio.js'; setFixtureDirectory(); @@ -22,6 +23,18 @@ const testResultNoBuffer = async (t, options) => { test('Sets empty result.ipcOutput if buffer is false', testResultNoBuffer, {buffer: false}); test('Sets empty result.ipcOutput if buffer is false, fd-specific buffer', testResultNoBuffer, {buffer: {ipc: false}}); +test('buffer.fd3 is invalid without stdio[3], even with ipc', t => { + const {message} = t.throws(() => { + execa('ipc-send-twice.js', {ipc: true, buffer: {fd3: false}}); + }); + t.true(message.includes('"buffer.fd3" is invalid: that file descriptor does not exist.')); +}); + +test('buffer.fd3 does not affect result.ipcOutput', async t => { + const {ipcOutput} = await execa('ipc-send-twice.js', {ipc: true, buffer: {fd3: false}, ...fullStdio}); + t.deepEqual(ipcOutput, foobarArray); +}); + test('Can use IPC methods when buffer is false', async t => { const subprocess = execa('ipc-send.js', {ipc: true, buffer: false}); t.is(await subprocess.getOneMessage(), foobarString); diff --git a/test/verbose/complete.js b/test/verbose/complete.js index b37e4b6eb1..21c09b73c1 100644 --- a/test/verbose/complete.js +++ b/test/verbose/complete.js @@ -11,6 +11,7 @@ import { getCompletionLines, testTimestamp, getVerboseOption, + getStdioForFd3Option, stdoutNoneOption, stdoutShortOption, stdoutFullOption, @@ -28,7 +29,7 @@ import { setFixtureDirectory(); const testPrintCompletion = async (t, verbose, isSync) => { - const {stderr} = await nestedSubprocess('noop.js', [foobarString], {verbose, isSync}); + const {stderr} = await nestedSubprocess('noop.js', [foobarString], {verbose, ...getStdioForFd3Option(verbose), isSync}); t.is(getCompletionLine(stderr), `${testTimestamp} [0] √ (done in 0ms)`); }; @@ -54,24 +55,24 @@ test('Prints completion, verbose "short", fd-specific ipc, sync', testPrintCompl test('Prints completion, verbose "full", fd-specific ipc, sync', testPrintCompletion, ipcFullOption, true); const testNoPrintCompletion = async (t, verbose, isSync) => { - const {stderr} = await nestedSubprocess('noop.js', [foobarString], {verbose, isSync}); + const {stderr} = await nestedSubprocess('noop.js', [foobarString], {verbose, ...getStdioForFd3Option(verbose), isSync}); t.is(stderr, ''); }; test('Does not print completion, verbose "none"', testNoPrintCompletion, 'none', false); -test('Does not print completion, verbose default"', testNoPrintCompletion, undefined, false); +test('Does not print completion, verbose default', testNoPrintCompletion, undefined, false); test('Does not print completion, verbose "none", fd-specific stdout', testNoPrintCompletion, stdoutNoneOption, false); test('Does not print completion, verbose "none", fd-specific stderr', testNoPrintCompletion, stderrNoneOption, false); test('Does not print completion, verbose "none", fd-specific fd3', testNoPrintCompletion, fd3NoneOption, false); test('Does not print completion, verbose "none", fd-specific ipc', testNoPrintCompletion, ipcNoneOption, false); -test('Does not print completion, verbose default", fd-specific', testNoPrintCompletion, {}, false); +test('Does not print completion, verbose default, fd-specific', testNoPrintCompletion, {}, false); test('Does not print completion, verbose "none", sync', testNoPrintCompletion, 'none', true); -test('Does not print completion, verbose default", sync', testNoPrintCompletion, undefined, true); +test('Does not print completion, verbose default, sync', testNoPrintCompletion, undefined, true); test('Does not print completion, verbose "none", fd-specific stdout, sync', testNoPrintCompletion, stdoutNoneOption, true); test('Does not print completion, verbose "none", fd-specific stderr, sync', testNoPrintCompletion, stderrNoneOption, true); test('Does not print completion, verbose "none", fd-specific fd3, sync', testNoPrintCompletion, fd3NoneOption, true); test('Does not print completion, verbose "none", fd-specific ipc, sync', testNoPrintCompletion, ipcNoneOption, true); -test('Does not print completion, verbose default", fd-specific, sync', testNoPrintCompletion, {}, true); +test('Does not print completion, verbose default, fd-specific, sync', testNoPrintCompletion, {}, true); const testPrintCompletionError = async (t, isSync) => { const stderr = await runErrorSubprocess(t, 'short', isSync); diff --git a/test/verbose/error.js b/test/verbose/error.js index 82d1a8021b..65bad87371 100644 --- a/test/verbose/error.js +++ b/test/verbose/error.js @@ -1,6 +1,6 @@ import test from 'ava'; import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; -import {foobarString, foobarRed} from '../helpers/input.js'; +import {foobarString} from '../helpers/input.js'; import {nestedSubprocess} from '../helpers/nested.js'; import { QUOTE, @@ -10,6 +10,7 @@ import { getErrorLines, testTimestamp, getVerboseOption, + getStdioForFd3Option, stdoutNoneOption, stdoutShortOption, stdoutFullOption, @@ -26,8 +27,10 @@ import { setFixtureDirectory(); +const redFoobarString = `\u001B[31m${foobarString}\u001B[39m`; + const testPrintError = async (t, verbose, isSync) => { - const stderr = await runErrorSubprocess(t, verbose, isSync); + const stderr = await runErrorSubprocess(t, verbose, isSync, getStdioForFd3Option(verbose)); t.is(getErrorLine(stderr), `${testTimestamp} [0] × Command failed with exit code 2: noop-fail.js 1 ${foobarString}`); }; @@ -53,7 +56,7 @@ test('Prints error, verbose "short", fd-specific ipc, sync', testPrintError, ipc test('Prints error, verbose "full", fd-specific ipc, sync', testPrintError, ipcFullOption, true); const testNoPrintError = async (t, verbose, isSync) => { - const stderr = await runErrorSubprocess(t, verbose, isSync, false); + const stderr = await runErrorSubprocess(t, verbose, isSync, {expectExitCode: false, ...getStdioForFd3Option(verbose)}); t.is(getErrorLine(stderr), undefined); }; @@ -149,7 +152,7 @@ test('Does not escape internal characters from error', async t => { }); test('Escapes and strips color sequences from error', async t => { - const {stderr} = await t.throwsAsync(nestedSubprocess('noop-forever.js', [foobarRed], {parentFixture: 'nested-fail.js', verbose: 'short'})); + const {stderr} = await t.throwsAsync(nestedSubprocess('noop-forever.js', [redFoobarString], {parentFixture: 'nested-fail.js', verbose: 'short'})); t.deepEqual(getErrorLines(stderr), [ `${testTimestamp} [0] × Command was killed with SIGTERM (Termination): noop-forever.js ${QUOTE}\\u001b[31m${foobarString}\\u001b[39m${QUOTE}`, `${testTimestamp} [0] × ${foobarString}`, diff --git a/test/verbose/ipc.js b/test/verbose/ipc.js index a2ac942726..41986e8e67 100644 --- a/test/verbose/ipc.js +++ b/test/verbose/ipc.js @@ -1,9 +1,12 @@ import {on} from 'node:events'; import {inspect} from 'node:util'; import test from 'ava'; +import {red} from 'yoctocolors'; +import {execa} from '../../index.js'; import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; -import {foobarString, foobarRed, foobarObject} from '../helpers/input.js'; +import {foobarString, foobarObject} from '../helpers/input.js'; import {nestedSubprocess, nestedInstance} from '../helpers/nested.js'; +import {fullStdio} from '../helpers/stdio.js'; import { getIpcLine, getIpcLines, @@ -23,6 +26,19 @@ const testPrintIpc = async (t, verbose) => { test('Prints IPC, verbose "full"', testPrintIpc, 'full'); test('Prints IPC, verbose "full", fd-specific', testPrintIpc, ipcFullOption); +test('verbose.fd3 is invalid without stdio[3], even with ipc', t => { + const {message} = t.throws(() => { + execa('ipc-send.js', {ipc: true, verbose: {fd3: 'full'}}); + }); + t.true(message.includes('"verbose.fd3" is invalid: that file descriptor does not exist.')); +}); + +test('verbose.fd3 does not affect IPC', async t => { + const {nestedResult, stderr} = await nestedSubprocess('ipc-send.js', {ipc: true, verbose: {fd3: 'full'}, ...fullStdio}); + t.deepEqual(nestedResult.ipcOutput, [foobarString]); + t.is(getIpcLine(stderr), undefined); +}); + const testNoPrintIpc = async (t, verbose) => { const {stderr} = await nestedSubprocess('ipc-send.js', {ipc: true, verbose}); t.is(getIpcLine(stderr), undefined); @@ -83,7 +99,7 @@ test('Does not escape internal characters from IPC', async t => { }); test('Strips color sequences from IPC', async t => { - const {stderr} = await nestedSubprocess('ipc-send.js', [foobarRed], {ipc: true, verbose: 'full'}); + const {stderr} = await nestedSubprocess('ipc-send.js', [red(foobarString)], {ipc: true, verbose: 'full'}, {env: {FORCE_COLOR: '1', NO_COLOR: undefined}}); t.is(getIpcLine(stderr), `${testTimestamp} [0] * ${foobarString}`); }); diff --git a/test/verbose/output-enable.js b/test/verbose/output-enable.js index eef37ef032..a0e6bc489f 100644 --- a/test/verbose/output-enable.js +++ b/test/verbose/output-enable.js @@ -1,6 +1,7 @@ import test from 'ava'; +import {red} from 'yoctocolors'; import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; -import {foobarString, foobarRed, foobarUtf16Uint8Array} from '../helpers/input.js'; +import {foobarString, foobarUtf16Uint8Array} from '../helpers/input.js'; import {fullStdio} from '../helpers/stdio.js'; import {nestedSubprocess} from '../helpers/nested.js'; import { @@ -105,7 +106,7 @@ test('Does not escape internal characters from stdout', async t => { }); test('Strips color sequences from stdout', async t => { - const {stderr} = await nestedSubprocess('noop.js', [foobarRed], {verbose: 'full'}); + const {stderr} = await nestedSubprocess('noop.js', [red(foobarString)], {verbose: 'full'}, {env: {FORCE_COLOR: '1', NO_COLOR: undefined}}); t.is(getOutputLine(stderr), `${testTimestamp} [0] ${foobarString}`); }); diff --git a/test/verbose/start.js b/test/verbose/start.js index 44ba0f87bf..ec12438ea0 100644 --- a/test/verbose/start.js +++ b/test/verbose/start.js @@ -1,6 +1,6 @@ import test from 'ava'; import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; -import {foobarString, foobarRed} from '../helpers/input.js'; +import {foobarString} from '../helpers/input.js'; import {nestedSubprocess} from '../helpers/nested.js'; import { QUOTE, @@ -10,6 +10,7 @@ import { getCommandLines, testTimestamp, getVerboseOption, + getStdioForFd3Option, stdoutNoneOption, stdoutShortOption, stdoutFullOption, @@ -26,8 +27,15 @@ import { setFixtureDirectory(); +const redFoobarString = `\u001B[31m${foobarString}\u001B[39m`; + const testPrintCommand = async (t, verbose, worker, isSync) => { - const {stderr} = await nestedSubprocess('noop.js', [foobarString], {verbose, worker, isSync}); + const {stderr} = await nestedSubprocess('noop.js', [foobarString], { + verbose, + ...getStdioForFd3Option(verbose), + worker, + isSync, + }); t.is(getCommandLine(stderr), `${testTimestamp} [0] $ noop.js ${foobarString}`); }; @@ -73,7 +81,7 @@ test('Prints command, verbose "short", fd-specific ipc, worker, sync', testPrint test('Prints command, verbose "full", fd-specific ipc, worker, sync', testPrintCommand, ipcFullOption, true, true); const testNoPrintCommand = async (t, verbose, isSync) => { - const {stderr} = await nestedSubprocess('noop.js', [foobarString], {verbose, isSync}); + const {stderr} = await nestedSubprocess('noop.js', [foobarString], {verbose, ...getStdioForFd3Option(verbose), isSync}); t.is(stderr, ''); }; @@ -151,7 +159,7 @@ test('Does not escape internal characters from command', async t => { }); test('Escapes color sequences from command', async t => { - const {stderr} = await nestedSubprocess('noop.js', [foobarRed], {verbose: 'short'}); + const {stderr} = await nestedSubprocess('noop.js', [redFoobarString], {verbose: 'short'}); t.true(getCommandLine(stderr).includes(`${QUOTE}\\u001b[31m${foobarString}\\u001b[39m${QUOTE}`)); }); diff --git a/types/arguments/specific.d.ts b/types/arguments/specific.d.ts index fb89b80f2a..e3293dd538 100644 --- a/types/arguments/specific.d.ts +++ b/types/arguments/specific.d.ts @@ -29,26 +29,28 @@ type FdSpecificObjectOption< type FdNumberToFromOption< FdNumber extends string, GenericOptionKeys extends GenericFromOption, -> = FdNumber extends '1' - ? 'stdout' extends GenericOptionKeys - ? 'stdout' - : 'fd1' extends GenericOptionKeys - ? 'fd1' - : 'all' extends GenericOptionKeys - ? 'all' - : never - : FdNumber extends '2' - ? 'stderr' extends GenericOptionKeys - ? 'stderr' - : 'fd2' extends GenericOptionKeys - ? 'fd2' +> = FdNumber extends 'ipc' + ? 'ipc' extends GenericOptionKeys + ? 'ipc' + : never + : FdNumber extends '1' + ? 'stdout' extends GenericOptionKeys + ? 'stdout' + : 'fd1' extends GenericOptionKeys + ? 'fd1' : 'all' extends GenericOptionKeys ? 'all' : never - : `fd${FdNumber}` extends GenericOptionKeys - ? `fd${FdNumber}` - : 'ipc' extends GenericOptionKeys - ? 'ipc' + : FdNumber extends '2' + ? 'stderr' extends GenericOptionKeys + ? 'stderr' + : 'fd2' extends GenericOptionKeys + ? 'fd2' + : 'all' extends GenericOptionKeys + ? 'all' + : never + : `fd${FdNumber}` extends GenericOptionKeys + ? `fd${FdNumber}` : never; export {};