Skip to content

Commit 8edbbdc

Browse files
dr-jsisaacs
authored andcommitted
Support exec auto pick bin when all bin is alias
PR-URL: #1972 Credit: @dr-js Close: #1972 Reviewed-by: @isaacs
1 parent 6141de7 commit 8edbbdc

File tree

4 files changed

+85
-13
lines changed

4 files changed

+85
-13
lines changed

‎docs/content/commands/npm-exec.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ the package specifier provided as the first positional argument according
5454
to the following heuristic:
5555

5656
- If the package has a single entry in its `bin` field in `package.json`,
57-
then that command will be used.
57+
or if all entries are aliases of the same command, then that command
58+
will be used.
5859
- If the package has multiple `bin` entries, and one of them matches the
5960
unscoped portion of the `name` field, then that command will be used.
6061
- If this does not result in exactly one option (either because there are

‎docs/content/commands/npx.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ description: Run a command from a local or remote npm package
88

99
```bash
1010
npm exec -- <pkg>[@<version>] [args...]
11-
npm exec -p <pkg>[@<version>] -- <cmd> [args...]
11+
npm exec --package=<pkg>[@<version>] -- <cmd> [args...]
1212
npm exec -c '<cmd> [args...]'
13-
npm exec -p foo -c '<cmd> [args...]'
13+
npm exec --package=foo -c '<cmd> [args...]'
1414

1515
npx <pkg>[@<specifier>] [args...]
1616
npx -p <pkg>[@<specifier>] <cmd> [args...]
@@ -19,7 +19,8 @@ npx -p <pkg>[@<specifier>] -c '<cmd> [args...]'
1919

2020
alias: npm x, npx
2121

22-
-p <pkg> --package=<pkg> (may be specified multiple times)
22+
--package=<pkg> (may be specified multiple times)
23+
-p is a shorthand for --package only when using npx executable
2324
-c <cmd> --call=<cmd> (may not be mixed with positional arguments)
2425
```
2526

@@ -29,9 +30,9 @@ This command allows you to run an arbitrary command from an npm package
2930
(either one installed locally, or fetched remotely), in a similar context
3031
as running it via `npm run`.
3132

32-
Whatever packages are specified by the `--package` or `-p` option will be
33+
Whatever packages are specified by the `--package` option will be
3334
provided in the `PATH` of the executed command, along with any locally
34-
installed package executables. The `--package` or `-p` option may be
35+
installed package executables. The `--package` option may be
3536
specified multiple times, to execute the supplied command in an environment
3637
where all specified packages are available.
3738

@@ -47,13 +48,14 @@ only be considered a match if they have the exact same name and version as
4748
the local dependency.
4849

4950
If no `-c` or `--call` option is provided, then the positional arguments
50-
are used to generate the command string. If no `-p` or `--package` options
51+
are used to generate the command string. If no `--package` options
5152
are provided, then npm will attempt to determine the executable name from
5253
the package specifier provided as the first positional argument according
5354
to the following heuristic:
5455

5556
- If the package has a single entry in its `bin` field in `package.json`,
56-
then that command will be used.
57+
or if all entries are aliases of the same command, then that command
58+
will be used.
5759
- If the package has multiple `bin` entries, and one of them matches the
5860
unscoped portion of the `name` field, then that command will be used.
5961
- If this does not result in exactly one option (either because there are

‎lib/exec.js‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,15 @@ const manifestMissing = (tree, mani) => {
226226

227227
const getBinFromManifest = mani => {
228228
// if we have a bin matching (unscoped portion of) packagename, use that
229-
// otherwise if there's 1 bin, use that,
229+
// otherwise if there's 1 bin or all bin value is the same (alias), use that,
230230
// otherwise fail
231-
const bins = Object.entries(mani.bin || {})
232-
if (bins.length === 1)
233-
return bins[0][0]
231+
const bin = mani.bin || {}
232+
if (new Set(Object.values(bin)).size === 1)
233+
return Object.keys(bin)[0]
234234

235235
// XXX probably a util to parse this better?
236236
const name = mani.name.replace(/^@[^/]+\//, '')
237-
if (mani.bin && mani.bin[name])
237+
if (bin[name])
238238
return name
239239

240240
// XXX need better error message

‎test/lib/exec.js‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,75 @@ t.test('npm exec @foo/bar -- --some=arg, locally installed', async t => {
386386
}])
387387
})
388388

389+
t.test('npm exec @foo/bar, with same bin alias and no unscoped named bin, locally installed', async t => {
390+
const foobarManifest = {
391+
name: '@foo/bar',
392+
version: '1.2.3',
393+
bin: {
394+
baz: 'corge', // pick the first one
395+
qux: 'corge',
396+
quux: 'corge',
397+
}
398+
}
399+
const path = t.testdir({
400+
node_modules: {
401+
'@foo/bar': {
402+
'package.json': JSON.stringify(foobarManifest)
403+
}
404+
}
405+
})
406+
npm.localPrefix = path
407+
ARB_ACTUAL_TREE[path] = {
408+
children: new Map([['@foo/bar', { name: '@foo/bar', version: '1.2.3' }]])
409+
}
410+
MANIFESTS['@foo/bar'] = foobarManifest
411+
await exec(['@foo/bar'], er => {
412+
if (er) {
413+
throw er
414+
}
415+
})
416+
t.strictSame(MKDIRPS, [], 'no need to make any dirs')
417+
t.match(ARB_CTOR, [ { package: ['@foo/bar'], path } ])
418+
t.strictSame(ARB_REIFY, [], 'no need to reify anything')
419+
t.equal(PROGRESS_ENABLED, true, 'progress re-enabled')
420+
t.match(RUN_SCRIPTS, [{
421+
pkg: { scripts: { npx: 'baz' } },
422+
banner: false,
423+
path: process.cwd(),
424+
stdioString: true,
425+
event: 'npx',
426+
env: { PATH: process.env.PATH },
427+
stdio: 'inherit'
428+
}])
429+
})
430+
431+
t.test('npm exec @foo/bar, with different bin alias and no unscoped named bin, locally installed', t => {
432+
const path = t.testdir()
433+
npm.localPrefix = path
434+
ARB_ACTUAL_TREE[path] = {
435+
children: new Map([['@foo/bar', { name: '@foo/bar', version: '1.2.3' }]])
436+
}
437+
MANIFESTS['@foo/bar'] = {
438+
name: '@foo/bar',
439+
version: '1.2.3',
440+
bin: {
441+
foo: 'qux',
442+
corge: 'qux',
443+
baz: 'quux',
444+
},
445+
_from: 'foo@',
446+
_id: '@foo/[email protected]'
447+
}
448+
return t.rejects(exec(['@foo/bar'], er => {
449+
if (er) {
450+
throw er
451+
}
452+
}), {
453+
message: 'could not determine executable to run',
454+
pkgid: '@foo/[email protected]'
455+
})
456+
})
457+
389458
t.test('run command with 2 packages, need install, verify sort', t => {
390459
// test both directions, should use same install dir both times
391460
// also test the read() call here, verify that the prompts match

0 commit comments

Comments
 (0)