Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support exec auto pick bin when all bin is alias #1972

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]'
}
return t.rejects(exec(['@foo/bar'], er => {
if (er) {
throw er
}
}), {
message: 'could not determine executable to run',
pkgid: '@foo/[email protected]'
})
})

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