diff --git a/docs/content/commands/npm-exec.md b/docs/content/commands/npm-exec.md index 38fb1bf25af4b..c9de9933be3a5 100644 --- a/docs/content/commands/npm-exec.md +++ b/docs/content/commands/npm-exec.md @@ -54,7 +54,8 @@ the package specifier provided as the first positional argument according to the following heuristic: - If the package has a single entry in its `bin` field in `package.json`, - then that command will be used. + or if all entries are aliases of the same command, then that command + will be used. - If the package has multiple `bin` entries, and one of them matches the unscoped portion of the `name` field, then that command will be used. - If this does not result in exactly one option (either because there are diff --git a/docs/content/commands/npx.md b/docs/content/commands/npx.md index a5522c66e85aa..625ac3d8cd602 100644 --- a/docs/content/commands/npx.md +++ b/docs/content/commands/npx.md @@ -8,9 +8,9 @@ description: Run a command from a local or remote npm package ```bash npm exec -- [@] [args...] -npm exec -p [@] -- [args...] +npm exec --package=[@] -- [args...] npm exec -c ' [args...]' -npm exec -p foo -c ' [args...]' +npm exec --package=foo -c ' [args...]' npx [@] [args...] npx -p [@] [args...] @@ -19,7 +19,8 @@ npx -p [@] -c ' [args...]' alias: npm x, npx --p --package= (may be specified multiple times) +--package= (may be specified multiple times) +-p is a shorthand for --package only when using npx executable -c --call= (may not be mixed with positional arguments) ``` @@ -29,9 +30,9 @@ This command allows you to run an arbitrary command from an npm package (either one installed locally, or fetched remotely), in a similar context as running it via `npm run`. -Whatever packages are specified by the `--package` or `-p` option will be +Whatever packages are specified by the `--package` option will be provided in the `PATH` of the executed command, along with any locally -installed package executables. The `--package` or `-p` option may be +installed package executables. The `--package` option may be specified multiple times, to execute the supplied command in an environment where all specified packages are available. @@ -47,13 +48,14 @@ only be considered a match if they have the exact same name and version as the local dependency. If no `-c` or `--call` option is provided, then the positional arguments -are used to generate the command string. If no `-p` or `--package` options +are used to generate the command string. If no `--package` options are provided, then npm will attempt to determine the executable name from the package specifier provided as the first positional argument according to the following heuristic: - If the package has a single entry in its `bin` field in `package.json`, - then that command will be used. + or if all entries are aliases of the same command, then that command + will be used. - If the package has multiple `bin` entries, and one of them matches the unscoped portion of the `name` field, then that command will be used. - If this does not result in exactly one option (either because there are diff --git a/lib/exec.js b/lib/exec.js index 088a7c00eba31..6bcaf838ed327 100644 --- a/lib/exec.js +++ b/lib/exec.js @@ -226,15 +226,15 @@ const manifestMissing = (tree, mani) => { const getBinFromManifest = mani => { // if we have a bin matching (unscoped portion of) packagename, use that - // otherwise if there's 1 bin, use that, + // otherwise if there's 1 bin or all bin value is the same (alias), use that, // otherwise fail - const bins = Object.entries(mani.bin || {}) - if (bins.length === 1) - return bins[0][0] + const bin = mani.bin || {} + if (new Set(Object.values(bin)).size === 1) + return Object.keys(bin)[0] // XXX probably a util to parse this better? const name = mani.name.replace(/^@[^/]+\//, '') - if (mani.bin && mani.bin[name]) + if (bin[name]) return name // XXX need better error message diff --git a/test/lib/exec.js b/test/lib/exec.js index fb89776b55eaf..08592353ce36c 100644 --- a/test/lib/exec.js +++ b/test/lib/exec.js @@ -386,6 +386,75 @@ t.test('npm exec @foo/bar -- --some=arg, locally installed', async t => { }]) }) +t.test('npm exec @foo/bar, with same bin alias and no unscoped named bin, locally installed', async t => { + const foobarManifest = { + name: '@foo/bar', + version: '1.2.3', + bin: { + baz: 'corge', // pick the first one + qux: 'corge', + quux: 'corge', + } + } + const path = t.testdir({ + node_modules: { + '@foo/bar': { + 'package.json': JSON.stringify(foobarManifest) + } + } + }) + npm.localPrefix = path + ARB_ACTUAL_TREE[path] = { + children: new Map([['@foo/bar', { name: '@foo/bar', version: '1.2.3' }]]) + } + MANIFESTS['@foo/bar'] = foobarManifest + await exec(['@foo/bar'], er => { + if (er) { + throw er + } + }) + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.match(ARB_CTOR, [ { package: ['@foo/bar'], path } ]) + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'baz' } }, + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit' + }]) +}) + +t.test('npm exec @foo/bar, with different bin alias and no unscoped named bin, locally installed', t => { + const path = t.testdir() + npm.localPrefix = path + ARB_ACTUAL_TREE[path] = { + children: new Map([['@foo/bar', { name: '@foo/bar', version: '1.2.3' }]]) + } + MANIFESTS['@foo/bar'] = { + name: '@foo/bar', + version: '1.2.3', + bin: { + foo: 'qux', + corge: 'qux', + baz: 'quux', + }, + _from: 'foo@', + _id: '@foo/bar@1.2.3' + } + return t.rejects(exec(['@foo/bar'], er => { + if (er) { + throw er + } + }), { + message: 'could not determine executable to run', + pkgid: '@foo/bar@1.2.3' + }) +}) + t.test('run command with 2 packages, need install, verify sort', t => { // test both directions, should use same install dir both times // also test the read() call here, verify that the prompts match