Skip to content

Commit 3aba8d6

Browse files
committed
npx: add install prompt, handle options correctly
- handle previous npx options that are still possible to be handled, and print a warning if any deprecated/removed options are used. - expand shorthands properly in npx command line. - take existing npm options into account when determining placement of the -- argument. - document changes from previous versions of npx. PR-URL: #1596 Credit: @isaacs Close: #1596 Reviewed-by: @ruyadorno
1 parent 87d27d3 commit 3aba8d6

File tree

10 files changed

+470
-20
lines changed

10 files changed

+470
-20
lines changed

bin/npx-cli.js

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,123 @@ const cli = require('../lib/cli.js')
66
process.argv[1] = require.resolve('./npm-cli.js')
77
process.argv.splice(2, 0, 'exec')
88

9+
// TODO: remove the affordances for removed items in npm v9
10+
const removedSwitches = new Set([
11+
'always-spawn',
12+
'ignore-existing',
13+
'shell-auto-fallback'
14+
])
15+
16+
const removedOpts = new Set([
17+
'npm',
18+
'node-arg',
19+
'n'
20+
])
21+
22+
const removed = new Set([
23+
...removedSwitches,
24+
...removedOpts
25+
])
26+
27+
const { types, shorthands } = require('../lib/config/defaults.js')
28+
const npmSwitches = Object.entries(types)
29+
.filter(([key, type]) => type === Boolean ||
30+
(Array.isArray(type) && type.includes(Boolean)))
31+
.map(([key, type]) => key)
32+
33+
// things that don't take a value
34+
const switches = new Set([
35+
...removedSwitches,
36+
...npmSwitches,
37+
'no-install',
38+
'quiet',
39+
'q',
40+
'version',
41+
'v',
42+
'help',
43+
'h'
44+
])
45+
46+
// things that do take a value
47+
const opts = new Set([
48+
...removedOpts,
49+
'package',
50+
'p',
51+
'cache',
52+
'userconfig',
53+
'call',
54+
'c',
55+
'shell',
56+
'npm',
57+
'node-arg',
58+
'n'
59+
])
60+
961
// break out of loop when we find a positional argument or --
1062
// If we find a positional arg, we shove -- in front of it, and
1163
// let the normal npm cli handle the rest.
1264
let i
65+
let sawRemovedFlags = false
1366
for (i = 3; i < process.argv.length; i++) {
1467
const arg = process.argv[i]
1568
if (arg === '--') {
1669
break
1770
} else if (/^-/.test(arg)) {
18-
// `--package foo` treated the same as `--package=foo`
19-
if (!arg.includes('=')) {
20-
i++
71+
const [key, ...v] = arg.replace(/^-+/, '').split('=')
72+
73+
switch (key) {
74+
case 'p':
75+
process.argv[i] = ['--package', ...v].join('=')
76+
break
77+
78+
case 'shell':
79+
process.argv[i] = ['--script-shell', ...v].join('=')
80+
break
81+
82+
case 'no-install':
83+
process.argv[i] = '--yes=false'
84+
break
85+
86+
default:
87+
// resolve shorthands and run again
88+
if (shorthands[key] && !removed.has(key)) {
89+
const a = [...shorthands[key]]
90+
if (v.length) {
91+
a.push(v.join('='))
92+
}
93+
process.argv.splice(i, 1, ...a)
94+
i--
95+
continue
96+
}
97+
break
98+
}
99+
100+
if (removed.has(key)) {
101+
console.error(`npx: the --${key} argument has been removed.`)
102+
sawRemovedFlags = true
103+
process.argv.splice(i, 1)
104+
i--
105+
}
106+
107+
if (v.length === 0 && !switches.has(key) &&
108+
(opts.has(key) || !/^-/.test(process.argv[i + 1]))) {
109+
// value will be next argument, skip over it.
110+
if (removed.has(key)) {
111+
// also remove the value for the cut key.
112+
process.argv.splice(i + 1, 1)
113+
} else {
114+
i++
115+
}
21116
}
22-
continue
23117
} else {
24118
// found a positional arg, put -- in front of it, and we're done
25119
process.argv.splice(i, 0, '--')
26120
break
27121
}
28122
}
29123

124+
if (sawRemovedFlags) {
125+
console.error('See `npm help exec` for more information')
126+
}
127+
30128
cli(process)

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ where all specified packages are available.
4141

4242
If any requested packages are not present in the local project
4343
dependencies, then they are installed to a folder in the npm cache, which
44-
is added to the `PATH` environment variable in the executed process.
44+
is added to the `PATH` environment variable in the executed process. A
45+
prompt is printed (which can be suppressed by providing either `--yes` or
46+
`--no`).
47+
4548
Package names provided without a specifier will be matched with whatever
4649
version exists in the local project. Package names with a specifier will
4750
only be considered a match if they have the exact same name and version as
@@ -137,6 +140,34 @@ $ npm x -c 'eslint && say "hooray, lint passed"'
137140
$ npx -c 'eslint && say "hooray, lint passed"'
138141
```
139142

143+
### Compatibility with Older npx Versions
144+
145+
The `npx` binary was rewritten in npm v7.0.0, and the standalone `npx`
146+
package deprecated at that time. `npx` uses the `npm exec`
147+
command instead of a separate argument parser and install process, with
148+
some affordances to maintain backwards compatibility with the arguments it
149+
accepted in previous versions.
150+
151+
This resulted in some shifts in its functionality:
152+
153+
- Any `npm` config value may be provided.
154+
- To prevent security and user-experience problems from mistyping package
155+
names, `npx` prompts before installing anything. Suppress this
156+
prompt with the `-y` or `--yes` option.
157+
- The `--no-install` option is deprecated, and will be converted to `--no`.
158+
- Shell fallback functionality is removed, as it is not advisable.
159+
- The `-p` argument is a shorthand for `--parseable` in npm, but shorthand
160+
for `--package` in npx. This is maintained, but only for the `npx`
161+
executable.
162+
- The `--ignore-existing` option is removed. Locally installed bins are
163+
always present in the executed process `PATH`.
164+
- The `--npm` option is removed. `npx` will always use the `npm` it ships
165+
with.
166+
- The `--node-arg` and `-n` options are removed.
167+
- The `--always-spawn` option is redundant, and thus removed.
168+
- The `--shell` option is replaced with `--script-shell`, but maintained
169+
in the `npx` executable for backwards compatibility.
170+
140171
### See Also
141172

142173
* [npm run-script](/cli-commands/run-script)

docs/content/cli-commands/npx.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ where all specified packages are available.
4141

4242
If any requested packages are not present in the local project
4343
dependencies, then they are installed to a folder in the npm cache, which
44-
is added to the `PATH` environment variable in the executed process.
44+
is added to the `PATH` environment variable in the executed process. A
45+
prompt is printed (which can be suppressed by providing either `--yes` or
46+
`--no`).
47+
4548
Package names provided without a specifier will be matched with whatever
4649
version exists in the local project. Package names with a specifier will
4750
only be considered a match if they have the exact same name and version as
@@ -137,6 +140,34 @@ $ npm x -c 'eslint && say "hooray, lint passed"'
137140
$ npx -c 'eslint && say "hooray, lint passed"'
138141
```
139142

143+
### Compatibility with Older npx Versions
144+
145+
The `npx` binary was rewritten in npm v7.0.0, and the standalone `npx`
146+
package deprecated at that time. `npx` uses the `npm exec`
147+
command instead of a separate argument parser and install process, with
148+
some affordances to maintain backwards compatibility with the arguments it
149+
accepted in previous versions.
150+
151+
This resulted in some shifts in its functionality:
152+
153+
- Any `npm` config value may be provided.
154+
- To prevent security and user-experience problems from mistyping package
155+
names, `npx` prompts before installing anything. Suppress this
156+
prompt with the `-y` or `--yes` option.
157+
- The `--no-install` option is deprecated, and will be converted to `--no`.
158+
- Shell fallback functionality is removed, as it is not advisable.
159+
- The `-p` argument is a shorthand for `--parseable` in npm, but shorthand
160+
for `--package` in npx. This is maintained, but only for the `npx`
161+
executable.
162+
- The `--ignore-existing` option is removed. Locally installed bins are
163+
always present in the executed process `PATH`.
164+
- The `--npm` option is removed. `npx` will always use the `npm` it ships
165+
with.
166+
- The `--node-arg` and `-n` options are removed.
167+
- The `--always-spawn` option is redundant, and thus removed.
168+
- The `--shell` option is replaced with `--script-shell`, but maintained
169+
in the `npx` executable for backwards compatibility.
170+
140171
### See Also
141172

142173
* [npm run-script](/cli-commands/run-script)

lib/config/defaults.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ exports.types = {
311311
'ham-it-up': Boolean,
312312
heading: String,
313313
'if-present': Boolean,
314-
include: [Array, 'dev', 'optional', 'peer'],
314+
include: [Array, 'prod', 'dev', 'optional', 'peer'],
315315
'include-staged': Boolean,
316316
'ignore-prepublish': Boolean,
317317
'ignore-scripts': Boolean,
@@ -365,7 +365,7 @@ exports.types = {
365365
'save-prod': Boolean,
366366
scope: String,
367367
'script-shell': [null, String],
368-
'scripts-prepend-node-path': [false, true, 'auto', 'warn-only'],
368+
'scripts-prepend-node-path': [Boolean, 'auto', 'warn-only'],
369369
searchopts: String,
370370
searchexclude: [null, String],
371371
searchlimit: Number,
@@ -412,7 +412,7 @@ function getLocalAddresses () {
412412
}
413413

414414
exports.shorthands = {
415-
before: ['--enjoy-by'],
415+
'enjoy-by': ['--before'],
416416
c: ['--call'],
417417
s: ['--loglevel', 'silent'],
418418
d: ['--loglevel', 'info'],

lib/config/flat-options.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ const flatOptions = npm => npm.flatOptions || Object.freeze({
198198
},
199199
userAgent: npm.config.get('user-agent'),
200200

201+
// yes, it's fine, just do it, jeez, stop asking
202+
yes: npm.config.get('yes'),
203+
201204
...getScopesAndAuths(npm),
202205

203206
// npm fund exclusive option to select an item from a funding list

lib/exec.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@ const usage = usageUtil('exec',
66
'Run a command from a local or remote npm package.\n\n' +
77

88
'npm exec -- <pkg>[@<version>] [args...]\n' +
9-
'npm exec -p <pkg>[@<version>] -- <cmd> [args...]\n' +
9+
'npm exec --package=<pkg>[@<version>] -- <cmd> [args...]\n' +
1010
'npm exec -c \'<cmd> [args...]\'\n' +
11-
'npm exec -p foo -c \'<cmd> [args...]\'\n' +
11+
'npm exec --package=foo -c \'<cmd> [args...]\'\n' +
1212
'\n' +
1313
'npx <pkg>[@<specifier>] [args...]\n' +
1414
'npx -p <pkg>[@<specifier>] <cmd> [args...]\n' +
1515
'npx -c \'<cmd> [args...]\'\n' +
1616
'npx -p <pkg>[@<specifier>] -c \'<cmd> [args...]\'',
1717

18-
'\n-p <pkg> --package=<pkg> (may be specified multiple times)\n' +
18+
'\n--package=<pkg> (may be specified multiple times)\n' +
19+
'-p is a shorthand for --package only when using npx executable\n' +
1920
'-c <cmd> --call=<cmd> (may not be mixed with positional arguments)'
2021
)
2122

2223
const completion = require('./utils/completion/installed-shallow.js')
2324

25+
const { promisify } = require('util')
26+
const read = promisify(require('read'))
27+
2428
// it's like this:
2529
//
2630
// npm x pkg@version <-- runs the bin named "pkg" or the only bin if only 1
@@ -118,9 +122,25 @@ const exec = async args => {
118122
// add installDir/node_modules/.bin to pathArr
119123
const add = manis.filter(mani => manifestMissing(tree, mani))
120124
.map(mani => mani._from)
125+
.sort((a, b) => a.localeCompare(b))
121126

122127
// no need to install if already present
123128
if (add.length) {
129+
if (!npm.flatOptions.yes) {
130+
// set -n to always say no
131+
if (npm.flatOptions.yes === false) {
132+
throw 'canceled'
133+
}
134+
const addList = add.map(a => ` ${a.replace(/@$/, '')}`)
135+
.join('\n') + '\n'
136+
const prompt = `Need to install the following packages:\n${
137+
addList
138+
}Ok to proceed? `
139+
const confirm = await read({ prompt, default: 'y' })
140+
if (confirm.trim().toLowerCase().charAt(0) !== 'y') {
141+
throw 'canceled'
142+
}
143+
}
124144
await arb.reify({ add })
125145
}
126146
pathArr.unshift(resolve(installDir, 'node_modules/.bin'))

tap-snapshots/test-lib-config-flat-options.js-TAP.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,6 @@ Object {
120120
"userAgent": "user-agent",
121121
"viewer": "viewer",
122122
"which": undefined,
123+
"yes": undefined,
123124
}
124125
`

0 commit comments

Comments
 (0)