import path from 'node:path'; import process, {version} from 'node:process'; import {pathToFileURL} from 'node:url'; import test from 'ava'; import getNode from 'get-node'; import {execa, execaSync, execaNode} from '../../index.js'; import {FIXTURES_DIRECTORY} from '../helpers/fixtures-directory.js'; import {identity, fullStdio} from '../helpers/stdio.js'; import {foobarString} from '../helpers/input.js'; import {getDenoNodePath} from '../helpers/file-path.js'; process.chdir(FIXTURES_DIRECTORY); const runWithNodeOption = (file, commandArguments, options) => Array.isArray(commandArguments) ? execa(file, commandArguments, {...options, node: true}) : execa(file, {...options, node: true}); const runWithNodeOptionSync = (file, commandArguments, options) => Array.isArray(commandArguments) ? execaSync(file, commandArguments, {...options, node: true}) : execaSync(file, {...options, node: true}); const runWithIpc = (file, options) => execa('node', [file], {...options, ipc: true}); const testNodeSuccess = async (t, execaMethod) => { const {exitCode, stdout} = await execaMethod('noop.js', [foobarString]); t.is(exitCode, 0); t.is(stdout, foobarString); }; test('execaNode() succeeds', testNodeSuccess, execaNode); test('The "node" option succeeds', testNodeSuccess, runWithNodeOption); test('The "node" option succeeds - sync', testNodeSuccess, runWithNodeOptionSync); test('execaNode(options) succeeds', async t => { const {stdout} = await execaNode({stripFinalNewline: false})('noop.js', [foobarString]); t.is(stdout, `${foobarString}\n`); }); test('execaNode`...` succeeds', async t => { const {stdout} = await execaNode`noop.js ${foobarString}`; t.is(stdout, foobarString); }); test('execaNode().pipe(execaNode()) succeeds', async t => { const {stdout} = await execaNode('noop.js').pipe(execaNode('--version')); t.is(stdout, version); }); test('execaNode().pipe(execa()) requires using "node"', async t => { await t.throwsAsync(execaNode('noop.js').pipe(execa('--version'))); }); test('execaNode().pipe(...) requires using "node"', async t => { await t.throwsAsync(execaNode('noop.js').pipe('--version')); }); test('execaNode().pipe`...` requires using "node"', async t => { await t.throwsAsync(execaNode('noop.js').pipe`--version`); }); test('execaNode() cannot set the "node" option to false', t => { t.throws(() => { execaNode('empty.js', {node: false}); }, {message: /The "node" option cannot be false/}); }); const testDoubleNode = (t, nodePath, execaMethod) => { t.throws(() => { execaMethod(nodePath, ['noop.js']); }, {message: /does not need to be "node"/}); }; test('Cannot use "node" as binary - execaNode()', testDoubleNode, 'node', execaNode); test('Cannot use "node" as binary - "node" option', testDoubleNode, 'node', runWithNodeOption); test('Cannot use "node" as binary - "node" option sync', testDoubleNode, 'node', runWithNodeOptionSync); test('Cannot use path to "node" as binary - execaNode()', testDoubleNode, process.execPath, execaNode); test('Cannot use path to "node" as binary - "node" option', testDoubleNode, process.execPath, runWithNodeOption); test('Cannot use path to "node" as binary - "node" option sync', testDoubleNode, process.execPath, runWithNodeOptionSync); test('Cannot use deno style nodePath as binary - execaNode()', testDoubleNode, getDenoNodePath(), execaNode); test('Cannot use deno style nodePath as binary - "node" option', testDoubleNode, getDenoNodePath(), runWithNodeOption); test('Cannot use deno style nodePath as binary - "node" option sync', testDoubleNode, getDenoNodePath(), runWithNodeOptionSync); const getNodePath = async () => { const {path} = await getNode(TEST_NODE_VERSION); return path; }; const TEST_NODE_VERSION = '16.0.0'; const testNodePath = async (t, execaMethod, mapPath) => { const nodePath = mapPath(await getNodePath()); const {stdout} = await execaMethod('--version', [], {nodePath}); t.is(stdout, `v${TEST_NODE_VERSION}`); }; test.serial('The "nodePath" option can be used - execaNode()', testNodePath, execaNode, identity); test.serial('The "nodePath" option can be a file URL - execaNode()', testNodePath, execaNode, pathToFileURL); test.serial('The "nodePath" option can be used - "node" option', testNodePath, runWithNodeOption, identity); test.serial('The "nodePath" option can be a file URL - "node" option', testNodePath, runWithNodeOption, pathToFileURL); test.serial('The "nodePath" option can be used - "node" option sync', testNodePath, runWithNodeOptionSync, identity); test.serial('The "nodePath" option can be a file URL - "node" option sync', testNodePath, runWithNodeOptionSync, pathToFileURL); const testNodePathDefault = async (t, execaMethod) => { const {stdout} = await execaMethod('--version'); t.is(stdout, process.version); }; test('The "nodePath" option defaults to the current Node.js binary - execaNode()', testNodePathDefault, execaNode); test('The "nodePath" option defaults to the current Node.js binary - "node" option', testNodePathDefault, runWithNodeOption); test('The "nodePath" option defaults to the current Node.js binary - "node" option sync', testNodePathDefault, runWithNodeOptionSync); const testNodePathInvalid = (t, execaMethod) => { t.throws(() => { execaMethod('noop.js', [], {nodePath: true}); }, {message: /The "nodePath" option must be a string or a file URL/}); }; test('The "nodePath" option must be a string or URL - execaNode()', testNodePathInvalid, execaNode); test('The "nodePath" option must be a string or URL - "node" option', testNodePathInvalid, runWithNodeOption); test('The "nodePath" option must be a string or URL - "node" option sync', testNodePathInvalid, runWithNodeOptionSync); const testFormerNodePath = (t, execaMethod) => { t.throws(() => { execaMethod('noop.js', [], {execPath: process.execPath}); }, {message: /The "execPath" option has been removed/}); }; test('The "execPath" option cannot be used - execaNode()', testFormerNodePath, execaNode); test('The "execPath" option cannot be used - "node" option', testFormerNodePath, runWithNodeOption); test('The "execPath" option cannot be used - "node" option sync', testFormerNodePath, runWithNodeOptionSync); const nodePathArguments = ['-p', ['process.env.Path || process.env.PATH']]; const testSubprocessNodePath = async (t, execaMethod, mapPath) => { const nodePath = mapPath(await getNodePath()); const {stdout} = await execaMethod(...nodePathArguments, {nodePath}); t.true(stdout.includes(TEST_NODE_VERSION)); }; test.serial('The "nodePath" option impacts the subprocess - execaNode()', testSubprocessNodePath, execaNode, identity); test.serial('The "nodePath" option impacts the subprocess - "node" option', testSubprocessNodePath, runWithNodeOption, identity); test.serial('The "nodePath" option impacts the subprocess - "node" option sync', testSubprocessNodePath, runWithNodeOptionSync, identity); const testSubprocessNodePathDefault = async (t, execaMethod) => { const {stdout} = await execaMethod(...nodePathArguments); t.true(stdout.includes(path.dirname(process.execPath))); }; test('The "nodePath" option defaults to the current Node.js binary in the subprocess - execaNode()', testSubprocessNodePathDefault, execaNode); test('The "nodePath" option defaults to the current Node.js binary in the subprocess - "node" option', testSubprocessNodePathDefault, runWithNodeOption); test('The "nodePath" option defaults to the current Node.js binary in the subprocess - "node" option sync', testSubprocessNodePathDefault, runWithNodeOptionSync); test.serial('The "nodePath" option requires "node: true" to impact the subprocess', async t => { const nodePath = await getNodePath(); const {stdout} = await execa('node', nodePathArguments.flat(), {nodePath}); t.false(stdout.includes(TEST_NODE_VERSION)); }); const testSubprocessNodePathCwd = async (t, execaMethod) => { const nodePath = await getNodePath(); const cwd = path.dirname(path.dirname(nodePath)); const relativeExecPath = path.relative(cwd, nodePath); const {stdout} = await execaMethod(...nodePathArguments, {nodePath: relativeExecPath, cwd}); t.true(stdout.includes(TEST_NODE_VERSION)); }; test.serial('The "nodePath" option is relative to "cwd" when used in the subprocess - execaNode()', testSubprocessNodePathCwd, execaNode); test.serial('The "nodePath" option is relative to "cwd" when used in the subprocess - "node" option', testSubprocessNodePathCwd, runWithNodeOption); test.serial('The "nodePath" option is relative to "cwd" when used in the subprocess - "node" option sync', testSubprocessNodePathCwd, runWithNodeOptionSync); const testCwdNodePath = async (t, execaMethod) => { const nodePath = await getNodePath(); const cwd = path.dirname(path.dirname(nodePath)); const relativeExecPath = path.relative(cwd, nodePath); const {stdout} = await execaMethod('--version', [], {nodePath: relativeExecPath, cwd}); t.is(stdout, `v${TEST_NODE_VERSION}`); }; test.serial('The "nodePath" option is relative to "cwd" - execaNode()', testCwdNodePath, execaNode); test.serial('The "nodePath" option is relative to "cwd" - "node" option', testCwdNodePath, runWithNodeOption); test.serial('The "nodePath" option is relative to "cwd" - "node" option sync', testCwdNodePath, runWithNodeOptionSync); const testDenoExecPath = async (t, execaMethod) => { const {exitCode, stdout} = await execaMethod('noop.js', [], {nodePath: getDenoNodePath()}); t.is(exitCode, 0); t.is(stdout, foobarString); }; test('The deno style "nodePath" option can be used - execaNode()', testDenoExecPath, execaNode); test('The deno style "nodePath" option can be used - "node" option', testDenoExecPath, runWithNodeOption); test('The deno style "nodePath" option can be used - "node" option sync', testDenoExecPath, runWithNodeOptionSync); const testNodeOptions = async (t, execaMethod) => { const {stdout} = await execaMethod('empty.js', [], {nodeOptions: ['--version']}); t.is(stdout, process.version); }; test('The "nodeOptions" option can be used - execaNode()', testNodeOptions, execaNode); test('The "nodeOptions" option can be used - "node" option', testNodeOptions, runWithNodeOption); test('The "nodeOptions" option can be used - "node" option sync', testNodeOptions, runWithNodeOptionSync); const spawnNestedExecaNode = (realExecArgv, fakeExecArgv, execaMethod, nodeOptions) => execa( 'node', [...realExecArgv, 'nested-node.js', fakeExecArgv, execaMethod, nodeOptions, 'noop.js', foobarString], {...fullStdio, cwd: FIXTURES_DIRECTORY}, ); const testInspectRemoval = async (t, fakeExecArgv, execaMethod) => { const {stdout, stdio} = await spawnNestedExecaNode([], fakeExecArgv, execaMethod, ''); t.is(stdout, foobarString); t.is(stdio[3], ''); }; test('The "nodeOptions" option removes --inspect without a port when defined by current process - execaNode()', testInspectRemoval, '--inspect', 'execaNode'); test('The "nodeOptions" option removes --inspect without a port when defined by current process - "node" option', testInspectRemoval, '--inspect', 'nodeOption'); test('The "nodeOptions" option removes --inspect with a port when defined by current process - execaNode()', testInspectRemoval, '--inspect=9222', 'execaNode'); test('The "nodeOptions" option removes --inspect with a port when defined by current process - "node" option', testInspectRemoval, '--inspect=9222', 'nodeOption'); test('The "nodeOptions" option removes --inspect-brk without a port when defined by current process - execaNode()', testInspectRemoval, '--inspect-brk', 'execaNode'); test('The "nodeOptions" option removes --inspect-brk without a port when defined by current process - "node" option', testInspectRemoval, '--inspect-brk', 'nodeOption'); test('The "nodeOptions" option removes --inspect-brk with a port when defined by current process - execaNode()', testInspectRemoval, '--inspect-brk=9223', 'execaNode'); test('The "nodeOptions" option removes --inspect-brk with a port when defined by current process - "node" option', testInspectRemoval, '--inspect-brk=9223', 'nodeOption'); const testInspectDifferentPort = async (t, execaMethod) => { const {stdout, stdio} = await spawnNestedExecaNode(['--inspect=9225'], '', execaMethod, '--inspect=9224'); t.is(stdout, foobarString); t.true(stdio[3].includes('Debugger listening')); }; test.serial('The "nodeOptions" option allows --inspect with a different port even when defined by current process - execaNode()', testInspectDifferentPort, 'execaNode'); test.serial('The "nodeOptions" option allows --inspect with a different port even when defined by current process - "node" option', testInspectDifferentPort, 'nodeOption'); const testInspectSamePort = async (t, execaMethod) => { const {stdout, stdio} = await spawnNestedExecaNode(['--inspect=9226'], '', execaMethod, '--inspect=9226'); t.is(stdout, foobarString); t.true(stdio[3].includes('address already in use')); }; test.serial('The "nodeOptions" option forbids --inspect with the same port when defined by current process - execaNode()', testInspectSamePort, 'execaNode'); test.serial('The "nodeOptions" option forbids --inspect with the same port when defined by current process - "node" option', testInspectSamePort, 'nodeOption'); const testIpc = async (t, execaMethod, options) => { const subprocess = execaMethod('ipc-echo.js', [], options); await subprocess.sendMessage(foobarString); t.is(await subprocess.getOneMessage(), foobarString); const {stdio} = await subprocess; t.is(stdio.length, 4); t.is(stdio[3], undefined); }; test('execaNode() adds an ipc channel', testIpc, execaNode, {}); test('The "node" option adds an ipc channel', testIpc, runWithNodeOption, {}); test('The "ipc" option adds an ipc channel', testIpc, runWithIpc, {}); test('The "ipc" option works with "stdio: \'pipe\'"', testIpc, runWithIpc, {stdio: 'pipe'}); test('The "ipc" option works with "stdio: [\'pipe\', \'pipe\', \'pipe\']"', testIpc, runWithIpc, {stdio: ['pipe', 'pipe', 'pipe']}); test('The "ipc" option works with "stdio: [\'pipe\', \'pipe\', \'pipe\', \'ipc\']"', testIpc, runWithIpc, {stdio: ['pipe', 'pipe', 'pipe', 'ipc']}); test('The "ipc" option works with "stdout: \'pipe\'"', testIpc, runWithIpc, {stdout: 'pipe'}); const NO_SEND_MESSAGE = 'sendMessage() can only be used'; test('No ipc channel is added by default', async t => { const {message, stdio} = await t.throwsAsync(execa('node', ['ipc-send.js'])); t.true(message.includes(NO_SEND_MESSAGE)); t.is(stdio.length, 3); }); const testDisableIpc = async (t, execaMethod) => { const {failed, message, stdio} = await execaMethod('ipc-send.js', [], {ipc: false, reject: false}); t.true(failed); t.true(message.includes(NO_SEND_MESSAGE)); t.is(stdio.length, 3); }; test('Can disable "ipc" - execaNode()', testDisableIpc, execaNode); test('Can disable "ipc" - "node" option', testDisableIpc, runWithNodeOption); test('Can disable "ipc" - "node" option sync', testDisableIpc, runWithNodeOptionSync); const NO_IPC_MESSAGE = /The "ipc: true" option cannot be used/; const testNoIpcSync = (t, node) => { t.throws(() => { execaSync('node', ['ipc-send.js'], {ipc: true, node}); }, {message: NO_IPC_MESSAGE}); }; test('Cannot use "ipc: true" with execaSync()', testNoIpcSync, undefined); test('Cannot use "ipc: true" with execaSync() - "node: false"', testNoIpcSync, false); test('Cannot use "ipc: true" with execaSync() - "node: true"', t => { t.throws(() => { execaSync('ipc-send.js', {ipc: true, node: true}); }, {message: NO_IPC_MESSAGE}); }); const testNoShell = async (t, execaMethod) => { const {failed, message} = await execaMethod('node --version', [], {shell: true, reject: false}); t.true(failed); t.true(message.includes('MODULE_NOT_FOUND')); }; test('Cannot use "shell: true" - execaNode()', testNoShell, execaNode); test('Cannot use "shell: true" - "node" option', testNoShell, runWithNodeOption); test('Cannot use "shell: true" - "node" option sync', testNoShell, runWithNodeOptionSync);