diff --git a/lib/arguments/file-url.js b/lib/arguments/file-url.js index 6c4ea9a2d9..448f703717 100644 --- a/lib/arguments/file-url.js +++ b/lib/arguments/file-url.js @@ -2,7 +2,7 @@ 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); + const fileString = normalizeFileUrl(normalizeDenoExecPath(file)); if (typeof fileString !== 'string') { throw new TypeError(`${name} must be a string or a file URL: ${fileString}.`); @@ -11,5 +11,15 @@ export const safeNormalizeFileUrl = (file, name) => { return fileString; }; +// In Deno node:process execPath is a special object, not just a string: +// https://github.com/denoland/deno/blob/f460188e583f00144000aa0d8ade08218d47c3c1/ext/node/polyfills/process.ts#L344 +const normalizeDenoExecPath = file => isDenoExecPath(file) + ? file.toString() + : file; + +export const isDenoExecPath = file => typeof file !== 'string' + && file + && Object.getPrototypeOf(file) === String.prototype; + // Same but also allows other values, e.g. `boolean` for the `shell` option export const normalizeFileUrl = file => file instanceof URL ? fileURLToPath(file) : file; diff --git a/lib/pipe/pipe-arguments.js b/lib/pipe/pipe-arguments.js index a1c9e58dd4..9745a9e7a7 100644 --- a/lib/pipe/pipe-arguments.js +++ b/lib/pipe/pipe-arguments.js @@ -1,6 +1,7 @@ import {normalizeParameters} from '../methods/parameters.js'; import {getStartTime} from '../return/duration.js'; import {SUBPROCESS_OPTIONS, getToStream, getFromStream} from '../arguments/fd-options.js'; +import {isDenoExecPath} from '../arguments/file-url.js'; // Normalize and validate arguments passed to `source.pipe(destination)` export const normalizePipeArguments = ({source, sourcePromise, boundOptions, createNested}, ...pipeArguments) => { @@ -56,7 +57,7 @@ const getDestination = (boundOptions, createNested, firstArgument, ...pipeArgume return {destination, pipeOptions: boundOptions}; } - if (typeof firstArgument === 'string' || firstArgument instanceof URL) { + if (typeof firstArgument === 'string' || firstArgument instanceof URL || isDenoExecPath(firstArgument)) { if (Object.keys(boundOptions).length > 0) { throw new TypeError('Please use .pipe("file", ..., options) or .pipe(execa("file", ..., options)) instead of .pipe(options)("file", ...).'); } diff --git a/test/helpers/file-path.js b/test/helpers/file-path.js index dea9fa97e3..db4032ff38 100644 --- a/test/helpers/file-path.js +++ b/test/helpers/file-path.js @@ -1,4 +1,15 @@ import path from 'node:path'; +import process from 'node:process'; export const getAbsolutePath = file => ({file}); export const getRelativePath = filePath => ({file: path.relative('.', filePath)}); +// Defined as getter so call to toString is not cached +export const getDenoNodePath = () => Object.freeze({ + __proto__: String.prototype, + toString() { + return process.execPath; + }, + get length() { + return this.toString().length; + }, +}); diff --git a/test/methods/node.js b/test/methods/node.js index 05eff2ec06..a9bca52933 100644 --- a/test/methods/node.js +++ b/test/methods/node.js @@ -7,6 +7,7 @@ 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); @@ -73,6 +74,9 @@ test('Cannot use "node" as binary - "node" option sync', testDoubleNode, 'node', 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); @@ -174,6 +178,16 @@ test.serial('The "nodePath" option is relative to "cwd" - execaNode()', testCwdN 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); diff --git a/test/pipe/pipe-arguments.js b/test/pipe/pipe-arguments.js index 291eb0728f..60a17401d0 100644 --- a/test/pipe/pipe-arguments.js +++ b/test/pipe/pipe-arguments.js @@ -4,6 +4,7 @@ import test from 'ava'; import {$, execa} from '../../index.js'; import {setFixtureDirectory, FIXTURES_DIRECTORY} from '../helpers/fixtures-directory.js'; import {foobarString} from '../helpers/input.js'; +import {getDenoNodePath} from '../helpers/file-path.js'; setFixtureDirectory(); @@ -73,6 +74,16 @@ test('execa.$.pipe("file", commandArguments, options)`', async t => { t.is(stdout, foobarString); }); +test('execa.$.pipe("file", commandArguments, options with denoNodePath)`', async t => { + const {stdout} = await execa('noop.js', [foobarString]).pipe('node', ['stdin.js'], {cwd: FIXTURES_DIRECTORY, nodePath: getDenoNodePath()}); + t.is(stdout, foobarString); +}); + +test('execa.$.pipe("file", commandArguments, denoNodePath)`', async t => { + const {stdout} = await execa('noop.js', [foobarString]).pipe(getDenoNodePath(), ['stdin.js'], {cwd: FIXTURES_DIRECTORY}); + t.is(stdout, foobarString); +}); + test('$.pipe.pipe("file", commandArguments, options)', async t => { const {stdout} = await $`noop.js ${foobarString}` .pipe`stdin.js`