Skip to content

Commit

Permalink
Support exec auto pick bin when all bin is alias
Browse files Browse the repository at this point in the history
PR-URL: #1972
Credit: @dr-js
Close: #1972
Reviewed-by: @isaacs
  • Loading branch information
dr-js authored and isaacs committed Nov 10, 2020
1 parent 6141de7 commit 8edbbdc
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/content/commands/npm-exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 9 additions & 7 deletions docs/content/commands/npx.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ description: Run a command from a local or remote npm package

```bash
npm exec -- <pkg>[@<version>] [args...]
npm exec -p <pkg>[@<version>] -- <cmd> [args...]
npm exec --package=<pkg>[@<version>] -- <cmd> [args...]
npm exec -c '<cmd> [args...]'
npm exec -p foo -c '<cmd> [args...]'
npm exec --package=foo -c '<cmd> [args...]'

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

alias: npm x, npx

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

Expand All @@ -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.

Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions lib/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions test/lib/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8edbbdc

Please sign in to comment.