Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/execa
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v9.5.0
Choose a base ref
...
head repository: sindresorhus/execa
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: c8cff27a47b6e6f1cfbfec2bf7fa9dcd08cefed1
Choose a head ref
  • 4 commits
  • 11 files changed
  • 2 contributors

Commits on Oct 28, 2024

  1. Upgrade which (#1168)

    ehmicky authored Oct 28, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    ab886ff View commit details
  2. Do not write verbose logs synchronously (#1167)

    ehmicky authored Oct 28, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    e3572d0 View commit details

Commits on Oct 29, 2024

  1. Runs CI tests on Node 23 (#1169)

    ehmicky authored Oct 29, 2024
    Copy the full SHA
    e330bd8 View commit details
  2. 9.5.1

    ehmicky committed Oct 29, 2024
    Copy the full SHA
    c8cff27 View commit details
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ jobs:
fail-fast: false
matrix:
node-version:
- 22
- 23
- 18
os:
- ubuntu
@@ -31,7 +31,7 @@ jobs:
with:
args: --cache --verbose --no-progress --include-fragments --exclude packagephobia --exclude /pull/ --exclude linkedin --exclude file:///test --exclude invalid.com '*.md' 'docs/*.md' '.github/**/*.md' '*.json' '*.js' 'lib/**/*.js' 'test/**/*.js' '*.ts' 'test-d/**/*.ts'
fail: true
if: ${{ matrix.os == 'ubuntu' && matrix.node-version == 22 }}
if: ${{ matrix.os == 'ubuntu' && matrix.node-version == 23 }}
- run: npm run lint
- run: npm run type
- run: npm run unit
17 changes: 11 additions & 6 deletions lib/verbose/log.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import {writeFileSync} from 'node:fs';
import {inspect} from 'node:util';
import {escapeLines} from '../arguments/escape.js';
import {defaultVerboseFunction} from './default.js';
import {applyVerboseOnLines} from './custom.js';

// Write synchronously to ensure lines are properly ordered and not interleaved with `stdout`
// This prints on stderr.
// If the subprocess prints on stdout and is using `stdout: 'inherit'`,
// there is a chance both writes will compete (introducing a race condition).
// This means their respective order is not deterministic.
// In particular, this means the verbose command lines might be after the start of the subprocess output.
// Using synchronous I/O does not solve this problem.
// However, this only seems to happen when the stdout/stderr target
// (e.g. a terminal) is being written to by many subprocesses at once, which is unlikely in real scenarios.
export const verboseLog = ({type, verboseMessage, fdNumber, verboseInfo, result}) => {
const verboseObject = getVerboseObject({type, result, verboseInfo});
const printedLines = getPrintedLines(verboseMessage, verboseObject);
const finalLines = applyVerboseOnLines(printedLines, verboseInfo, fdNumber);
writeFileSync(STDERR_FD, finalLines);
if (finalLines !== '') {
console.warn(finalLines.slice(0, -1));
}
};

const getVerboseObject = ({
@@ -35,9 +43,6 @@ const getPrintedLine = verboseObject => {
return {verboseLine, verboseObject};
};

// Unless a `verbose` function is used, print all logs on `stderr`
const STDERR_FD = 2;

// Serialize any type to a line string, for logging
export const serializeVerboseMessage = message => {
const messageString = typeof message === 'string' ? message : inspect(message);
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "execa",
"version": "9.5.0",
"version": "9.5.1",
"description": "Process execution for humans",
"license": "MIT",
"repository": "sindresorhus/execa",
@@ -76,7 +76,7 @@
"tempfile": "^5.0.0",
"tsd": "^0.31.0",
"typescript": "^5.4.5",
"which": "^4.0.0",
"which": "^5.0.0",
"xo": "^0.59.3"
},
"c8": {
23 changes: 16 additions & 7 deletions test/convert/duplex.js
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import {
getReadWriteSubprocess,
} from '../helpers/convert.js';
import {foobarString} from '../helpers/input.js';
import {majorNodeVersion} from '../helpers/node-version.js';
import {prematureClose, fullStdio, fullReadableStdio} from '../helpers/stdio.js';
import {defaultHighWaterMark} from '../helpers/stream.js';

@@ -148,13 +149,21 @@ test('.duplex() can pipe to errored stream with Stream.pipeline()', async t => {
const cause = new Error('test');
outputStream.destroy(cause);

await assertPromiseError(t, pipeline(inputStream, stream, outputStream), cause);
await t.throwsAsync(finishedStream(stream));

await assertStreamError(t, inputStream, cause);
const error = await assertStreamError(t, stream, cause);
await assertStreamReadError(t, outputStream, cause);
await assertSubprocessError(t, subprocess, {cause: error});
// Node 23 does not allow calling `stream.pipeline()` with an already errored stream
if (majorNodeVersion >= 23) {
outputStream.on('error', () => {});
stream.on('error', () => {});
await t.throwsAsync(pipeline(stream, outputStream), {code: 'ERR_STREAM_UNABLE_TO_PIPE'});
stream.end();
} else {
await assertPromiseError(t, pipeline(inputStream, stream, outputStream), cause);
await t.throwsAsync(finishedStream(stream));

await assertStreamError(t, inputStream, cause);
const error = await assertStreamError(t, stream, cause);
await assertStreamReadError(t, outputStream, cause);
await assertSubprocessError(t, subprocess, {cause: error});
}
});

test('.duplex() can be piped to errored stream with Stream.pipeline()', async t => {
23 changes: 14 additions & 9 deletions test/convert/readable.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {once} from 'node:events';
import process from 'node:process';
import {
compose,
Readable,
@@ -29,6 +28,7 @@ import {
} from '../helpers/convert.js';
import {foobarString, foobarBuffer, foobarObject} from '../helpers/input.js';
import {simpleFull} from '../helpers/lines.js';
import {majorNodeVersion} from '../helpers/node-version.js';
import {prematureClose, fullStdio} from '../helpers/stdio.js';
import {outputObjectGenerator, getOutputsAsyncGenerator} from '../helpers/generator.js';
import {defaultHighWaterMark, defaultObjectHighWaterMark} from '../helpers/stream.js';
@@ -231,12 +231,18 @@ test('.readable() can pipe to errored stream with Stream.pipeline()', async t =>
const cause = new Error('test');
outputStream.destroy(cause);

await assertPromiseError(t, pipeline(stream, outputStream), cause);
await t.throwsAsync(finishedStream(stream));

const error = await assertStreamError(t, stream, cause);
await assertStreamReadError(t, outputStream, cause);
await assertSubprocessError(t, subprocess, {cause: error});
// Node 23 does not allow calling `stream.pipeline()` with an already errored stream
if (majorNodeVersion >= 23) {
outputStream.on('error', () => {});
await t.throwsAsync(pipeline(stream, outputStream), {code: 'ERR_STREAM_UNABLE_TO_PIPE'});
} else {
await assertPromiseError(t, pipeline(stream, outputStream), cause);
await t.throwsAsync(finishedStream(stream));

const error = await assertStreamError(t, stream, cause);
await assertStreamReadError(t, outputStream, cause);
await assertSubprocessError(t, subprocess, {cause: error});
}
});

test('.readable() can be used with Stream.compose()', async t => {
@@ -421,8 +427,7 @@ test('.duplex() can be paused', async t => {

// This feature does not work on Node 18.
// @todo: remove after dropping support for Node 18.
const majorVersion = Number(process.version.split('.')[0].slice(1));
if (majorVersion >= 20) {
if (majorNodeVersion >= 20) {
const testHighWaterMark = async (t, methodName) => {
const subprocess = execa('stdin.js');
const stream = subprocess[methodName]();
3 changes: 1 addition & 2 deletions test/fixtures/graceful-ref.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env node
import {once} from 'node:events';
import {getCancelSignal} from 'execa';

const cancelSignal = await getCancelSignal();
once(cancelSignal, 'abort');
cancelSignal.addEventListener('abort', () => {});
3 changes: 3 additions & 0 deletions test/helpers/node-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {version} from 'node:process';

export const majorNodeVersion = Number(version.split('.')[0].slice(1));
63 changes: 63 additions & 0 deletions test/stdio/type-invalid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import test from 'ava';
import {execa, execaSync} from '../../index.js';
import {getStdio} from '../helpers/stdio.js';
import {noopGenerator} from '../helpers/generator.js';
import {generatorsMap} from '../helpers/map.js';
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';

setFixtureDirectory();

const testInvalidGenerator = (t, fdNumber, stdioOption, execaMethod) => {
t.throws(() => {
execaMethod('empty.js', getStdio(fdNumber, {...noopGenerator(), ...stdioOption}));
}, {message: 'final' in stdioOption ? /must be a generator/ : /must be a generator, a Duplex stream or a web TransformStream/});
};

test('Cannot use invalid "transform" with stdin', testInvalidGenerator, 0, {transform: true}, execa);
test('Cannot use invalid "transform" with stdout', testInvalidGenerator, 1, {transform: true}, execa);
test('Cannot use invalid "transform" with stderr', testInvalidGenerator, 2, {transform: true}, execa);
test('Cannot use invalid "transform" with stdio[*]', testInvalidGenerator, 3, {transform: true}, execa);
test('Cannot use invalid "final" with stdin', testInvalidGenerator, 0, {final: true}, execa);
test('Cannot use invalid "final" with stdout', testInvalidGenerator, 1, {final: true}, execa);
test('Cannot use invalid "final" with stderr', testInvalidGenerator, 2, {final: true}, execa);
test('Cannot use invalid "final" with stdio[*]', testInvalidGenerator, 3, {final: true}, execa);
test('Cannot use invalid "transform" with stdin, sync', testInvalidGenerator, 0, {transform: true}, execaSync);
test('Cannot use invalid "transform" with stdout, sync', testInvalidGenerator, 1, {transform: true}, execaSync);
test('Cannot use invalid "transform" with stderr, sync', testInvalidGenerator, 2, {transform: true}, execaSync);
test('Cannot use invalid "transform" with stdio[*], sync', testInvalidGenerator, 3, {transform: true}, execaSync);
test('Cannot use invalid "final" with stdin, sync', testInvalidGenerator, 0, {final: true}, execaSync);
test('Cannot use invalid "final" with stdout, sync', testInvalidGenerator, 1, {final: true}, execaSync);
test('Cannot use invalid "final" with stderr, sync', testInvalidGenerator, 2, {final: true}, execaSync);
test('Cannot use invalid "final" with stdio[*], sync', testInvalidGenerator, 3, {final: true}, execaSync);

// eslint-disable-next-line max-params
const testInvalidBinary = (t, fdNumber, optionName, type, execaMethod) => {
t.throws(() => {
execaMethod('empty.js', getStdio(fdNumber, {...generatorsMap[type].uppercase(), [optionName]: 'true'}));
}, {message: /a boolean/});
};

test('Cannot use invalid "binary" with stdin', testInvalidBinary, 0, 'binary', 'generator', execa);
test('Cannot use invalid "binary" with stdout', testInvalidBinary, 1, 'binary', 'generator', execa);
test('Cannot use invalid "binary" with stderr', testInvalidBinary, 2, 'binary', 'generator', execa);
test('Cannot use invalid "binary" with stdio[*]', testInvalidBinary, 3, 'binary', 'generator', execa);
test('Cannot use invalid "objectMode" with stdin, generators', testInvalidBinary, 0, 'objectMode', 'generator', execa);
test('Cannot use invalid "objectMode" with stdout, generators', testInvalidBinary, 1, 'objectMode', 'generator', execa);
test('Cannot use invalid "objectMode" with stderr, generators', testInvalidBinary, 2, 'objectMode', 'generator', execa);
test('Cannot use invalid "objectMode" with stdio[*], generators', testInvalidBinary, 3, 'objectMode', 'generator', execa);
test('Cannot use invalid "binary" with stdin, sync', testInvalidBinary, 0, 'binary', 'generator', execaSync);
test('Cannot use invalid "binary" with stdout, sync', testInvalidBinary, 1, 'binary', 'generator', execaSync);
test('Cannot use invalid "binary" with stderr, sync', testInvalidBinary, 2, 'binary', 'generator', execaSync);
test('Cannot use invalid "binary" with stdio[*], sync', testInvalidBinary, 3, 'binary', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdin, generators, sync', testInvalidBinary, 0, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdout, generators, sync', testInvalidBinary, 1, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stderr, generators, sync', testInvalidBinary, 2, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdio[*], generators, sync', testInvalidBinary, 3, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdin, duplexes', testInvalidBinary, 0, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stdout, duplexes', testInvalidBinary, 1, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stderr, duplexes', testInvalidBinary, 2, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stdio[*], duplexes', testInvalidBinary, 3, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stdin, webTransforms', testInvalidBinary, 0, 'objectMode', 'webTransform', execa);
test('Cannot use invalid "objectMode" with stdout, webTransforms', testInvalidBinary, 1, 'objectMode', 'webTransform', execa);
test('Cannot use invalid "objectMode" with stderr, webTransforms', testInvalidBinary, 2, 'objectMode', 'webTransform', execa);
test('Cannot use invalid "objectMode" with stdio[*], webTransforms', testInvalidBinary, 3, 'objectMode', 'webTransform', execa);
57 changes: 1 addition & 56 deletions test/stdio/type.js → test/stdio/type-undefined.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,14 @@
import test from 'ava';
import {execa, execaSync} from '../../index.js';
import {getStdio} from '../helpers/stdio.js';
import {noopGenerator, uppercaseGenerator} from '../helpers/generator.js';
import {uppercaseGenerator} from '../helpers/generator.js';
import {uppercaseBufferDuplex} from '../helpers/duplex.js';
import {uppercaseBufferWebTransform} from '../helpers/web-transform.js';
import {generatorsMap} from '../helpers/map.js';
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';

setFixtureDirectory();

const testInvalidGenerator = (t, fdNumber, stdioOption, execaMethod) => {
t.throws(() => {
execaMethod('empty.js', getStdio(fdNumber, {...noopGenerator(), ...stdioOption}));
}, {message: 'final' in stdioOption ? /must be a generator/ : /must be a generator, a Duplex stream or a web TransformStream/});
};

test('Cannot use invalid "transform" with stdin', testInvalidGenerator, 0, {transform: true}, execa);
test('Cannot use invalid "transform" with stdout', testInvalidGenerator, 1, {transform: true}, execa);
test('Cannot use invalid "transform" with stderr', testInvalidGenerator, 2, {transform: true}, execa);
test('Cannot use invalid "transform" with stdio[*]', testInvalidGenerator, 3, {transform: true}, execa);
test('Cannot use invalid "final" with stdin', testInvalidGenerator, 0, {final: true}, execa);
test('Cannot use invalid "final" with stdout', testInvalidGenerator, 1, {final: true}, execa);
test('Cannot use invalid "final" with stderr', testInvalidGenerator, 2, {final: true}, execa);
test('Cannot use invalid "final" with stdio[*]', testInvalidGenerator, 3, {final: true}, execa);
test('Cannot use invalid "transform" with stdin, sync', testInvalidGenerator, 0, {transform: true}, execaSync);
test('Cannot use invalid "transform" with stdout, sync', testInvalidGenerator, 1, {transform: true}, execaSync);
test('Cannot use invalid "transform" with stderr, sync', testInvalidGenerator, 2, {transform: true}, execaSync);
test('Cannot use invalid "transform" with stdio[*], sync', testInvalidGenerator, 3, {transform: true}, execaSync);
test('Cannot use invalid "final" with stdin, sync', testInvalidGenerator, 0, {final: true}, execaSync);
test('Cannot use invalid "final" with stdout, sync', testInvalidGenerator, 1, {final: true}, execaSync);
test('Cannot use invalid "final" with stderr, sync', testInvalidGenerator, 2, {final: true}, execaSync);
test('Cannot use invalid "final" with stdio[*], sync', testInvalidGenerator, 3, {final: true}, execaSync);

// eslint-disable-next-line max-params
const testInvalidBinary = (t, fdNumber, optionName, type, execaMethod) => {
t.throws(() => {
execaMethod('empty.js', getStdio(fdNumber, {...generatorsMap[type].uppercase(), [optionName]: 'true'}));
}, {message: /a boolean/});
};

test('Cannot use invalid "binary" with stdin', testInvalidBinary, 0, 'binary', 'generator', execa);
test('Cannot use invalid "binary" with stdout', testInvalidBinary, 1, 'binary', 'generator', execa);
test('Cannot use invalid "binary" with stderr', testInvalidBinary, 2, 'binary', 'generator', execa);
test('Cannot use invalid "binary" with stdio[*]', testInvalidBinary, 3, 'binary', 'generator', execa);
test('Cannot use invalid "objectMode" with stdin, generators', testInvalidBinary, 0, 'objectMode', 'generator', execa);
test('Cannot use invalid "objectMode" with stdout, generators', testInvalidBinary, 1, 'objectMode', 'generator', execa);
test('Cannot use invalid "objectMode" with stderr, generators', testInvalidBinary, 2, 'objectMode', 'generator', execa);
test('Cannot use invalid "objectMode" with stdio[*], generators', testInvalidBinary, 3, 'objectMode', 'generator', execa);
test('Cannot use invalid "binary" with stdin, sync', testInvalidBinary, 0, 'binary', 'generator', execaSync);
test('Cannot use invalid "binary" with stdout, sync', testInvalidBinary, 1, 'binary', 'generator', execaSync);
test('Cannot use invalid "binary" with stderr, sync', testInvalidBinary, 2, 'binary', 'generator', execaSync);
test('Cannot use invalid "binary" with stdio[*], sync', testInvalidBinary, 3, 'binary', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdin, generators, sync', testInvalidBinary, 0, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdout, generators, sync', testInvalidBinary, 1, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stderr, generators, sync', testInvalidBinary, 2, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdio[*], generators, sync', testInvalidBinary, 3, 'objectMode', 'generator', execaSync);
test('Cannot use invalid "objectMode" with stdin, duplexes', testInvalidBinary, 0, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stdout, duplexes', testInvalidBinary, 1, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stderr, duplexes', testInvalidBinary, 2, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stdio[*], duplexes', testInvalidBinary, 3, 'objectMode', 'duplex', execa);
test('Cannot use invalid "objectMode" with stdin, webTransforms', testInvalidBinary, 0, 'objectMode', 'webTransform', execa);
test('Cannot use invalid "objectMode" with stdout, webTransforms', testInvalidBinary, 1, 'objectMode', 'webTransform', execa);
test('Cannot use invalid "objectMode" with stderr, webTransforms', testInvalidBinary, 2, 'objectMode', 'webTransform', execa);
test('Cannot use invalid "objectMode" with stdio[*], webTransforms', testInvalidBinary, 3, 'objectMode', 'webTransform', execa);

// eslint-disable-next-line max-params
const testUndefinedOption = (t, fdNumber, optionName, type, optionValue) => {
t.throws(() => {
7 changes: 3 additions & 4 deletions test/terminate/kill-signal.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {once} from 'node:events';
import {platform, version} from 'node:process';
import {platform} from 'node:process';
import {constants} from 'node:os';
import {setImmediate} from 'node:timers/promises';
import test from 'ava';
import {execa, execaSync} from '../../index.js';
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';
import {majorNodeVersion} from '../helpers/node-version.js';

setFixtureDirectory();

const isWindows = platform === 'win32';
const majorNodeVersion = Number(version.split('.')[0].slice(1));

const testKillSignal = async (t, killSignal) => {
const {isTerminated, signal} = await t.throwsAsync(execa('forever.js', {killSignal, timeout: 1}));
@@ -61,11 +61,10 @@ test('Can call `.kill()` multiple times', async t => {
subprocess.kill();

const {exitCode, isTerminated, signal, code} = await t.throwsAsync(subprocess);

// On Windows, calling `subprocess.kill()` twice emits an `error` event on the subprocess.
// This does not happen when passing an `error` argument, nor when passing a non-terminating signal.
// There is no easy way to make this cross-platform, so we document the difference here.
if (isWindows && majorNodeVersion >= 22) {
if (isWindows && majorNodeVersion === 22) {
t.is(exitCode, undefined);
t.false(isTerminated);
t.is(signal, undefined);
Loading