diff --git a/bin/npx-cli.js b/bin/npx-cli.js index e2e1b87906abe..296bf3edb17fa 100755 --- a/bin/npx-cli.js +++ b/bin/npx-cli.js @@ -24,6 +24,7 @@ const removed = new Set([ ...removedOpts, ]) +const nopt = require('nopt') const { definitions, shorthands } = require('@npmcli/config/lib/definitions') const npmSwitches = Object.entries(definitions) .filter(([, { type }]) => type === Boolean || @@ -43,6 +44,17 @@ const switches = new Set([ 'h', ]) +// a negated switch (--no-color, or --no expanded to --no-yes) only takes the +// next argument when npm reads it as the value, as in --no-color always +const readsNegatedValue = (negation, next) => { + if (next === undefined) { + return false + } + const key = negation.slice(3) + const { argv } = nopt({ [key]: definitions[key].type }, {}, [`--${negation}`, next], 0) + return argv.remain.length === 0 +} + // things that do take a value const opts = new Set([ ...removedOpts, @@ -105,8 +117,11 @@ for (i = 3; i < process.argv.length; i++) { i-- } + const negated = key.startsWith('no-') && npmSwitches.includes(key.slice(3)) + if (v.length === 0 && !switches.has(key) && - (opts.has(key) || !/^-/.test(process.argv[i + 1]))) { + (opts.has(key) || !/^-/.test(process.argv[i + 1])) && + (!negated || readsNegatedValue(key, process.argv[i + 1]))) { // value will be next argument, skip over it. if (removed.has(key)) { // also remove the value for the cut key. diff --git a/test/bin/npx-cli.js b/test/bin/npx-cli.js index 67a8d3319fc18..4fa3440dbd4d0 100644 --- a/test/bin/npx-cli.js +++ b/test/bin/npx-cli.js @@ -38,6 +38,53 @@ t.test('npx --x=y --no-install foo -z -> npm exec --x=y -- foo -z', async t => { t.strictSame(argv, ['node', npm, 'exec', '--x=y', '--yes=false', '--', 'foo', '-z']) }) +t.test('npx --no foo -z -> npm exec --no-yes -- foo -z', async t => { + const { argv } = mockNpx(t, ['node', npx, '--no', 'foo', '-z']) + t.strictSame(argv, ['node', npm, 'exec', '--no-yes', '--', 'foo', '-z']) +}) + +t.test('negated switches do not take the command as a value', async t => { + const { argv } = mockNpx(t, ['node', npx, '--no-color', '--local', 'foo', '--version']) + t.strictSame(argv, ['node', npm, 'exec', '--no-color', '--no-global', '--', 'foo', '--version']) +}) + +t.test('negated switches keep values that npm reads for them', async t => { + const { argv } = mockNpx(t, [ + 'node', + npx, + '--no=false', + '--no-color', + 'always', + '--no-workspaces', + 'null', + '--no-browser', + 'firefox', + 'foo', + '-z', + ]) + t.strictSame(argv, [ + 'node', + npm, + 'exec', + '--no-yes', + 'false', + '--no-color', + 'always', + '--no-workspaces', + 'null', + '--no-browser', + 'firefox', + '--', + 'foo', + '-z', + ]) +}) + +t.test('npx --no -> npm exec --no-yes', async t => { + const { argv } = mockNpx(t, ['node', npx, '--no']) + t.strictSame(argv, ['node', npm, 'exec', '--no-yes']) +}) + t.test('transform renamed options into proper values', async t => { const { argv } = mockNpx(t, ['node', npx, '-y', '--shell=bash', '-p', 'foo', '-c', 'asdf']) t.strictSame(argv, [