diff --git a/test/lib/commands/uninstall.js b/test/lib/commands/uninstall.js index 049bf2da8b1c3..0302aa5bac30b 100644 --- a/test/lib/commands/uninstall.js +++ b/test/lib/commands/uninstall.js @@ -143,6 +143,34 @@ t.test('remove multiple installed libs', async t => { t.throws(() => fs.statSync(b), 'should have removed b package from nm') }) +t.test('rejects an arg with a version spec', async t => { + const { uninstall } = await mockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'test-rm-version-spec', + version: '1.0.0', + dependencies: { + foo: '*', + }, + }), + node_modules: { + foo: { + 'package.json': JSON.stringify({ + name: 'foo', + version: '1.0.0', + }), + }, + }, + }, + }) + + await t.rejects( + uninstall(['foo@1']), + { code: 'ERMARGS', message: /npm rm foo/ }, + 'should throw ERMARGS instead of silently no-oping' + ) +}) + t.test('no args local', async t => { const { uninstall } = await mockNpm(t) diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 2082f7d48372e..7ff7a21cf9b6f 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -257,6 +257,22 @@ module.exports = cls => class IdealTreeBuilder extends cls { this[_updateNames] = update.names this[_updateAll] = update.all + + // validates list of rm names, they must + // be dep names only, no semver ranges are supported + for (const name of options.rm || []) { + const spec = npa(name) + const validationError = + new TypeError(`Remove arguments must only contain package names, eg: + npm rm ${spec.name || ''}`) + validationError.code = 'ERMARGS' + + // If they gave us anything other than a bare package name + if (spec.raw !== spec.name) { + throw validationError + } + } + // we prune by default unless explicitly set to boolean false this.#prune = options.prune !== false diff --git a/workspaces/arborist/test/arborist/build-ideal-tree.js b/workspaces/arborist/test/arborist/build-ideal-tree.js index 4ab079ef9984e..ad4eae06a81a8 100644 --- a/workspaces/arborist/test/arborist/build-ideal-tree.js +++ b/workspaces/arborist/test/arborist/build-ideal-tree.js @@ -2389,6 +2389,47 @@ t.test('remove deps when initializing tree from actual tree', async t => { t.equal(tree.children.get('foo'), undefined, 'removed foo child') }) +t.test('remove deps with a version spec', async t => { + const path = t.testdir({ + node_modules: { + foo: { + 'package.json': JSON.stringify({ + name: 'foo', + version: '1.2.3', + }), + }, + }, + }) + + createRegistry(t, false) + const invalidArgs = [ + 'foo@1.2.3', + 'foo@next', + 'foo@^1.0.0', + 'foo@>=2.0.0', + 'foo@2', + ] + for (const rmName of invalidArgs) { + await t.rejects( + buildIdeal(path, { rm: [rmName] }), + { code: 'ERMARGS', message: /npm rm foo/ }, + 'should throw an error when the package name has a version' + ) + } + + await t.rejects( + buildIdeal(path, { rm: ['@scope/foo@1.2.3'] }), + { code: 'ERMARGS', message: /npm rm @scope\/foo/ }, + 'should throw an error when a scoped package name has a version' + ) + + await t.rejects( + buildIdeal(path, { rm: ['./foo'] }), + { code: 'ERMARGS', message: /npm rm / }, + 'should throw an error when the package is a path' + ) +}) + t.test('detect conflicts in transitive peerOptional deps', async t => { const base = resolve(fixtures, 'test-conflicted-optional-peer-dep')