Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export {ExecaError, ExecaSyncError} from './types/return/final-error.js';

export {execa, type ExecaMethod} from './types/methods/main-async.js';
export {execaSync, type ExecaSyncMethod} from './types/methods/main-sync.js';
export {execaCommand, execaCommandSync, parseCommandString} from './types/methods/command.js';
export {parseCommandString} from './types/methods/command.js';
export {$, type ExecaScriptMethod, type ExecaScriptSyncMethod} from './types/methods/script.js';
export {execaNode, type ExecaNodeMethod} from './types/methods/node.js';

Expand Down
3 changes: 0 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {createExeca} from './lib/methods/create.js';
import {mapCommandAsync, mapCommandSync} from './lib/methods/command.js';
import {mapNode} from './lib/methods/node.js';
import {mapScriptAsync, setScriptSync, deepScriptOptions} from './lib/methods/script.js';
import {getIpcExport} from './lib/ipc/methods.js';
Expand All @@ -9,8 +8,6 @@ export {ExecaError, ExecaSyncError} from './lib/return/final-error.js';

export const execa = createExeca(() => ({}));
export const execaSync = createExeca(() => ({isSync: true}));
export const execaCommand = createExeca(mapCommandAsync);
export const execaCommandSync = createExeca(mapCommandSync);
export const execaNode = createExeca(mapNode);
export const $ = createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);

Expand Down
16 changes: 0 additions & 16 deletions lib/methods/command.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
// 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}.`);
}

const [file, ...commandArguments] = parseCommandString(command);
return {file, commandArguments};
};

// Convert `command` string into an array of file or arguments to pass to $`${...fileOrCommandArguments}`
export const parseCommandString = command => {
if (typeof command !== 'string') {
Expand Down
8 changes: 3 additions & 5 deletions lib/methods/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,12 @@ const parseArguments = ({mapArguments, firstArgument, nextArguments, deepOptions
const [initialFile, initialArguments, initialOptions] = normalizeParameters(...callArguments);
const mergedOptions = mergeOptions(mergeOptions(deepOptions, boundOptions), initialOptions);
const {
file = initialFile,
commandArguments = initialArguments,
options = mergedOptions,
isSync = false,
} = mapArguments({file: initialFile, commandArguments: initialArguments, options: mergedOptions});
} = mapArguments({options: mergedOptions});
return {
file,
commandArguments,
file: initialFile,
commandArguments: initialArguments,
options,
isSync,
};
Expand Down
98 changes: 1 addition & 97 deletions test-d/methods/command.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import {expectType, expectError, expectAssignable} from 'tsd';
import {expectType, expectError} from 'tsd';
import {
execa,
execaSync,
$,
execaNode,
execaCommand,
execaCommandSync,
parseCommandString,
type Result,
type ResultPromise,
type SyncResult,
} from '../../index.js';

const fileUrl = new URL('file:///test');
const stringArray = ['foo', 'bar'] as const;

expectError(parseCommandString());
expectError(parseCommandString(true));
expectError(parseCommandString(['unicorns', 'arg']));
Expand All @@ -31,93 +25,3 @@ expectType<Result<{}>>(await execaNode`${parseCommandString('foo bar')}`);
expectType<Result<{}>>(await execa`unicorns ${parseCommandString('foo bar')}`);
expectType<Result<{}>>(await execa('unicorns', parseCommandString('foo bar')));
expectType<Result<{}>>(await execa('unicorns', ['foo', ...parseCommandString('bar')]));

expectError(execaCommand());
expectError(execaCommand(true));
expectError(execaCommand(['unicorns', 'arg']));
expectAssignable<ResultPromise>(execaCommand('unicorns'));
expectError(execaCommand(fileUrl));

expectError(execaCommand('unicorns', []));
expectError(execaCommand('unicorns', ['foo']));
expectError(execaCommand('unicorns', 'foo'));
expectError(execaCommand('unicorns', [true]));

expectAssignable<ResultPromise>(execaCommand('unicorns', {}));
expectError(execaCommand('unicorns', [], {}));
expectError(execaCommand('unicorns', [], []));
expectError(execaCommand('unicorns', {other: true}));

expectAssignable<ResultPromise>(execaCommand`unicorns`);
expectType<Result<{}>>(await execaCommand('unicorns'));
expectType<Result<{}>>(await execaCommand`unicorns`);

expectAssignable<typeof execaCommand>(execaCommand({}));
expectAssignable<ResultPromise>(execaCommand({})('unicorns'));
expectAssignable<ResultPromise>(execaCommand({})`unicorns`);

expectAssignable<{stdout: string}>(await execaCommand('unicorns'));
expectAssignable<{stdout: Uint8Array}>(await execaCommand('unicorns', {encoding: 'buffer'}));
expectAssignable<{stdout: string}>(await execaCommand({})('unicorns'));
expectAssignable<{stdout: Uint8Array}>(await execaCommand({encoding: 'buffer'})('unicorns'));
expectAssignable<{stdout: Uint8Array}>(await execaCommand({})({encoding: 'buffer'})('unicorns'));
expectAssignable<{stdout: Uint8Array}>(await execaCommand({encoding: 'buffer'})({})('unicorns'));
expectAssignable<{stdout: string}>(await execaCommand({})`unicorns`);
expectAssignable<{stdout: Uint8Array}>(await execaCommand({encoding: 'buffer'})`unicorns`);
expectAssignable<{stdout: Uint8Array}>(await execaCommand({})({encoding: 'buffer'})`unicorns`);
expectAssignable<{stdout: Uint8Array}>(await execaCommand({encoding: 'buffer'})({})`unicorns`);

expectType<Result<{}>>(await execaCommand`${'unicorns'}`);
expectType<Result<{}>>(await execaCommand`unicorns ${'foo'}`);
expectError(await execaCommand`unicorns ${'foo'} ${'bar'}`);
expectError(await execaCommand`unicorns ${1}`);
expectError(await execaCommand`unicorns ${stringArray}`);
expectError(await execaCommand`unicorns ${[1, 2]}`);
expectType<Result<{}>>(await execaCommand`unicorns ${false.toString()}`);
expectError(await execaCommand`unicorns ${false}`);

expectError(await execaCommand`unicorns ${await execaCommand`echo foo`}`);
expectError(await execaCommand`unicorns ${await execaCommand({reject: false})`echo foo`}`);
expectError(await execaCommand`unicorns ${execaCommand`echo foo`}`);
expectError(await execaCommand`unicorns ${[await execaCommand`echo foo`, 'bar']}`);
expectError(await execaCommand`unicorns ${[execaCommand`echo foo`, 'bar']}`);

expectError(execaCommandSync());
expectError(execaCommandSync(true));
expectError(execaCommandSync(['unicorns', 'arg']));
expectType<SyncResult<{}>>(execaCommandSync('unicorns'));
expectError(execaCommandSync(fileUrl));
expectError(execaCommandSync('unicorns', []));
expectError(execaCommandSync('unicorns', ['foo']));
expectType<SyncResult<{}>>(execaCommandSync('unicorns', {}));
expectError(execaCommandSync('unicorns', [], {}));
expectError(execaCommandSync('unicorns', 'foo'));
expectError(execaCommandSync('unicorns', [true]));
expectError(execaCommandSync('unicorns', [], []));
expectError(execaCommandSync('unicorns', {other: true}));
expectType<SyncResult<{}>>(execaCommandSync`unicorns`);
expectAssignable<typeof execaCommandSync>(execaCommandSync({}));
expectType<SyncResult<{}>>(execaCommandSync({})('unicorns'));
expectType<SyncResult<{}>>(execaCommandSync({})`unicorns`);
expectType<SyncResult<{}>>(execaCommandSync('unicorns'));
expectType<SyncResult<{}>>(execaCommandSync`unicorns`);
expectAssignable<{stdout: string}>(execaCommandSync('unicorns'));
expectAssignable<{stdout: Uint8Array}>(execaCommandSync('unicorns', {encoding: 'buffer'}));
expectAssignable<{stdout: string}>(execaCommandSync({})('unicorns'));
expectAssignable<{stdout: Uint8Array}>(execaCommandSync({encoding: 'buffer'})('unicorns'));
expectAssignable<{stdout: Uint8Array}>(execaCommandSync({})({encoding: 'buffer'})('unicorns'));
expectAssignable<{stdout: Uint8Array}>(execaCommandSync({encoding: 'buffer'})({})('unicorns'));
expectAssignable<{stdout: string}>(execaCommandSync({})`unicorns`);
expectAssignable<{stdout: Uint8Array}>(execaCommandSync({encoding: 'buffer'})`unicorns`);
expectAssignable<{stdout: Uint8Array}>(execaCommandSync({})({encoding: 'buffer'})`unicorns`);
expectAssignable<{stdout: Uint8Array}>(execaCommandSync({encoding: 'buffer'})({})`unicorns`);
expectType<SyncResult<{}>>(execaCommandSync`${'unicorns'}`);
expectType<SyncResult<{}>>(execaCommandSync`unicorns ${'foo'}`);
expectError(execaCommandSync`unicorns ${'foo'} ${'bar'}`);
expectError(execaCommandSync`unicorns ${1}`);
expectError(execaCommandSync`unicorns ${stringArray}`);
expectError(execaCommandSync`unicorns ${[1, 2]}`);
expectError(execaCommandSync`unicorns ${execaCommandSync`echo foo`}`);
expectError(execaCommandSync`unicorns ${[execaCommandSync`echo foo`, 'bar']}`);
expectType<SyncResult<{}>>(execaCommandSync`unicorns ${false.toString()}`);
expectError(execaCommandSync`unicorns ${false}`);
122 changes: 15 additions & 107 deletions test/methods/command.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,31 @@
import path from 'node:path';
import test from 'ava';
import {
execa,
execaSync,
$,
execaNode,
execaCommand,
execaCommandSync,
parseCommandString,
} from '../../index.js';
import {
setFixtureDirectory,
FIXTURES_DIRECTORY,
FIXTURES_DIRECTORY_URL,
} from '../helpers/fixtures-directory.js';
import {QUOTE} from '../helpers/verbose.js';

setFixtureDirectory();
const STDIN_FIXTURE = path.join(FIXTURES_DIRECTORY, 'stdin.js');
const ECHO_FIXTURE_URL = new URL('echo.js', FIXTURES_DIRECTORY_URL);

const parseAndRunCommand = command => execa`${parseCommandString(command)}`;

test('execaCommand()', async t => {
const {stdout} = await execaCommand('echo.js foo bar');
t.is(stdout, 'foo\nbar');
});

test('parseCommandString() + execa()', async t => {
const {stdout} = await execa('echo.js', parseCommandString('foo bar'));
t.is(stdout, 'foo\nbar');
});

test('execaCommandSync()', t => {
const {stdout} = execaCommandSync('echo.js foo bar');
t.is(stdout, 'foo\nbar');
});

test('parseCommandString() + execaSync()', t => {
const {stdout} = execaSync('echo.js', parseCommandString('foo bar'));
t.is(stdout, 'foo\nbar');
});

test('execaCommand`...`', async t => {
const {stdout} = await execaCommand`${'echo.js foo bar'}`;
t.is(stdout, 'foo\nbar');
});

test('parseCommandString() + execa`...`', async t => {
const {stdout} = await execa`${parseCommandString('echo.js foo bar')}`;
t.is(stdout, 'foo\nbar');
Expand All @@ -62,11 +41,6 @@ test('parseCommandString() + execa`...`, only some arguments', async t => {
t.is(stdout, 'foo bar\nfoo\nbar');
});

test('execaCommandSync`...`', t => {
const {stdout} = execaCommandSync`${'echo.js foo bar'}`;
t.is(stdout, 'foo\nbar');
});

test('parseCommandString() + execaSync`...`', t => {
const {stdout} = execaSync`${parseCommandString('echo.js foo bar')}`;
t.is(stdout, 'foo\nbar');
Expand Down Expand Up @@ -97,98 +71,32 @@ test('parseCommandString() + execaNode', async t => {
t.is(stdout, 'foo\nbar');
});

test('execaCommand(options)`...`', async t => {
const {stdout} = await execaCommand({stripFinalNewline: false})`${'echo.js foo bar'}`;
t.is(stdout, 'foo\nbar\n');
});

test('execaCommandSync(options)`...`', t => {
const {stdout} = execaCommandSync({stripFinalNewline: false})`${'echo.js foo bar'}`;
t.is(stdout, 'foo\nbar\n');
});

test('execaCommand(options)()', async t => {
const {stdout} = await execaCommand({stripFinalNewline: false})('echo.js foo bar');
t.is(stdout, 'foo\nbar\n');
});

test('execaCommandSync(options)()', t => {
const {stdout} = execaCommandSync({stripFinalNewline: false})('echo.js foo bar');
t.is(stdout, 'foo\nbar\n');
});

test('execaCommand().pipe(execaCommand())', async t => {
const {stdout} = await execaCommand('echo.js foo bar').pipe(execaCommand(`node ${STDIN_FIXTURE}`));
t.is(stdout, 'foo\nbar');
});

test('execaCommand().pipe(...) does not use execaCommand', async t => {
const {escapedCommand} = await execaCommand('echo.js foo bar').pipe(`node ${STDIN_FIXTURE}`, {reject: false});
t.true(escapedCommand.startsWith(`${QUOTE}node `));
});

test('execaCommand() bound options have lower priority', async t => {
const {stdout} = await execaCommand({stripFinalNewline: false})('echo.js foo bar', {stripFinalNewline: true});
t.is(stdout, 'foo\nbar');
});

test('execaCommandSync() bound options have lower priority', t => {
const {stdout} = execaCommandSync({stripFinalNewline: false})('echo.js foo bar', {stripFinalNewline: true});
t.is(stdout, 'foo\nbar');
});

const testInvalidArgumentsArray = (t, execaMethod) => {
t.throws(() => execaMethod('echo', ['foo']), {
message: /The command and its arguments must be passed as a single string/,
});
};

test('execaCommand() must not pass an array of arguments', testInvalidArgumentsArray, execaCommand);
test('execaCommandSync() must not pass an array of arguments', testInvalidArgumentsArray, execaCommandSync);

const testInvalidArgumentsTemplate = (t, execaMethod) => {
t.throws(() => execaMethod`echo foo`, {
message: /The command and its arguments must be passed as a single string/,
});
};

test('execaCommand() must not pass an array of arguments with a template string', testInvalidArgumentsTemplate, execaCommand);
test('execaCommandSync() must not pass an array of arguments with a template string', testInvalidArgumentsTemplate, execaCommandSync);

const testInvalidArgumentsParse = (t, command) => {
t.throws(() => parseCommandString(command), {
message: /The command must be a string/,
});
};

test('execaCommand() must not pass a number', testInvalidArgumentsParse, 0);
test('execaCommand() must not pass undefined', testInvalidArgumentsParse, undefined);
test('execaCommand() must not pass null', testInvalidArgumentsParse, null);
test('execaCommand() must not pass a symbol', testInvalidArgumentsParse, Symbol('test'));
test('execaCommand() must not pass an object', testInvalidArgumentsParse, {});
test('execaCommand() must not pass an array', testInvalidArgumentsParse, []);
test('parseCommandString() must not pass a number', testInvalidArgumentsParse, 0);
test('parseCommandString() must not pass undefined', testInvalidArgumentsParse, undefined);
test('parseCommandString() must not pass null', testInvalidArgumentsParse, null);
test('parseCommandString() must not pass a symbol', testInvalidArgumentsParse, Symbol('test'));
test('parseCommandString() must not pass an object', testInvalidArgumentsParse, {});
test('parseCommandString() must not pass an array', testInvalidArgumentsParse, []);

const testExecaCommandOutput = async (t, command, expectedOutput, execaMethod) => {
const testParseCommandOutput = async (t, command, expectedOutput, execaMethod) => {
const {stdout} = await execaMethod(command);
t.is(stdout, expectedOutput);
};

test('execaCommand() allows escaping spaces in commands', testExecaCommandOutput, 'command\\ with\\ space.js foo bar', 'foo\nbar', execaCommand);
test('execaCommand() trims', testExecaCommandOutput, ' echo.js foo bar ', 'foo\nbar', execaCommand);
test('execaCommand() ignores consecutive spaces', testExecaCommandOutput, 'echo.js foo bar', 'foo\nbar', execaCommand);
test('execaCommand() escapes other whitespaces', testExecaCommandOutput, 'echo.js foo\tbar', 'foo\tbar', execaCommand);
test('execaCommand() allows escaping spaces', testExecaCommandOutput, 'echo.js foo\\ bar', 'foo bar', execaCommand);
test('execaCommand() allows escaping backslashes before spaces', testExecaCommandOutput, 'echo.js foo\\\\ bar', 'foo\\ bar', execaCommand);
test('execaCommand() allows escaping multiple backslashes before spaces', testExecaCommandOutput, 'echo.js foo\\\\\\\\ bar', 'foo\\\\\\ bar', execaCommand);
test('execaCommand() allows escaping backslashes not before spaces', testExecaCommandOutput, 'echo.js foo\\bar baz', 'foo\\bar\nbaz', execaCommand);
test('parseCommandString() allows escaping spaces in commands', testExecaCommandOutput, 'command\\ with\\ space.js foo bar', 'foo\nbar', parseAndRunCommand);
test('parseCommandString() trims', testExecaCommandOutput, ' echo.js foo bar ', 'foo\nbar', parseAndRunCommand);
test('parseCommandString() ignores consecutive spaces', testExecaCommandOutput, 'echo.js foo bar', 'foo\nbar', parseAndRunCommand);
test('parseCommandString() escapes other whitespaces', testExecaCommandOutput, 'echo.js foo\tbar', 'foo\tbar', parseAndRunCommand);
test('parseCommandString() allows escaping spaces', testExecaCommandOutput, 'echo.js foo\\ bar', 'foo bar', parseAndRunCommand);
test('parseCommandString() allows escaping backslashes before spaces', testExecaCommandOutput, 'echo.js foo\\\\ bar', 'foo\\ bar', parseAndRunCommand);
test('parseCommandString() allows escaping multiple backslashes before spaces', testExecaCommandOutput, 'echo.js foo\\\\\\\\ bar', 'foo\\\\\\ bar', parseAndRunCommand);
test('parseCommandString() allows escaping backslashes not before spaces', testExecaCommandOutput, 'echo.js foo\\bar baz', 'foo\\bar\nbaz', parseAndRunCommand);
test('parseCommandString() allows escaping spaces in commands', testParseCommandOutput, 'command\\ with\\ space.js foo bar', 'foo\nbar', parseAndRunCommand);
test('parseCommandString() trims', testParseCommandOutput, ' echo.js foo bar ', 'foo\nbar', parseAndRunCommand);
test('parseCommandString() ignores consecutive spaces', testParseCommandOutput, 'echo.js foo bar', 'foo\nbar', parseAndRunCommand);
test('parseCommandString() escapes other whitespaces', testParseCommandOutput, 'echo.js foo\tbar', 'foo\tbar', parseAndRunCommand);
test('parseCommandString() allows escaping spaces', testParseCommandOutput, 'echo.js foo\\ bar', 'foo bar', parseAndRunCommand);
test('parseCommandString() allows escaping backslashes before spaces', testParseCommandOutput, 'echo.js foo\\\\ bar', 'foo\\ bar', parseAndRunCommand);
test('parseCommandString() allows escaping multiple backslashes before spaces', testParseCommandOutput, 'echo.js foo\\\\\\\\ bar', 'foo\\\\\\ bar', parseAndRunCommand);
test('parseCommandString() allows escaping backslashes not before spaces', testParseCommandOutput, 'echo.js foo\\bar baz', 'foo\\bar\nbaz', parseAndRunCommand);

test('parseCommandString() can get empty strings', t => {
t.deepEqual(parseCommandString(''), []);
Expand Down
Loading
Loading