diff --git a/lib/uninstall.js b/lib/uninstall.js index dbaa992f500e0..4f63abfafc1c0 100644 --- a/lib/uninstall.js +++ b/lib/uninstall.js @@ -1,11 +1,18 @@ -// remove a package. +'use strict' +const { resolve } = require('path') const Arborist = require('@npmcli/arborist') -const npm = require('./npm.js') const rpj = require('read-package-json-fast') -const { resolve } = require('path') + +const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const reifyFinish = require('./utils/reify-finish.js') +const completion = require('./utils/completion/installed-shallow.js') + +const usage = usageUtil( + 'uninstall', + 'npm uninstall [<@scope>/][@]... [--save-prod|--save-dev|--save-optional] [--no-save]' +) const cmd = (args, cb) => rm(args).then(() => cb()).catch(cb) @@ -16,12 +23,19 @@ const rm = async args => { if (!args.length) { if (!global) - throw new Error('must provide a package name to remove') + throw new Error('Must provide a package name to remove') else { - const pkg = await rpj(resolve(npm.localPrefix, 'package.json')) - .catch(er => { - throw er.code !== 'ENOENT' && er.code !== 'ENOTDIR' ? er : usage() - }) + let pkg + + try { + pkg = await rpj(resolve(npm.localPrefix, 'package.json')) + } catch (er) { + if (er.code !== 'ENOENT' && er.code !== 'ENOTDIR') + throw er + else + throw usage + } + args.push(pkg.name) } } @@ -35,11 +49,4 @@ const rm = async args => { await reifyFinish(arb) } -const usage = usageUtil( - 'uninstall', - 'npm uninstall [<@scope>/][@]... [--save-prod|--save-dev|--save-optional] [--no-save]' -) - -const completion = require('./utils/completion/installed-shallow.js') - module.exports = Object.assign(cmd, { usage, completion }) diff --git a/test/lib/uninstall.js b/test/lib/uninstall.js new file mode 100644 index 0000000000000..69040c0f25aec --- /dev/null +++ b/test/lib/uninstall.js @@ -0,0 +1,253 @@ +const fs = require('fs') +const { resolve } = require('path') +const t = require('tap') +const requireInject = require('require-inject') + +const npm = { + globalDir: '', + flatOptions: { + global: false, + prefix: '', + }, + localPrefix: '', +} +const mocks = { + '../../lib/npm.js': npm, + '../../lib/utils/reify-finish.js': () => Promise.resolve(), + '../../lib/utils/usage.js': () => 'usage instructions', +} + +const uninstall = requireInject('../../lib/uninstall.js', mocks) + +t.afterEach(cb => { + npm.globalDir = '' + npm.prefix = '' + npm.flatOptions.global = false + npm.flatOptions.prefix = '' + cb() +}) + +t.test('remove single installed lib', t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-rm-single-lib', + version: '1.0.0', + dependencies: { + a: '*', + b: '*', + }, + }), + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.0.0', + }), + }, + }, + 'package-lock.json': JSON.stringify({ + name: 'test-rm-single-lib', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'test-rm-single-lib', + version: '1.0.0', + dependencies: { + a: '*', + }, + }, + 'node_modules/a': { + version: '1.0.0', + }, + 'node_modules/b': { + version: '1.0.0', + }, + }, + dependencies: { + a: { + version: '1.0.0', + }, + b: { + version: '1.0.0', + }, + }, + }), + }) + + const b = resolve(path, 'node_modules/b') + t.ok(() => fs.statSync(b)) + + npm.flatOptions.prefix = path + + uninstall(['b'], err => { + if (err) + throw err + + t.throws(() => fs.statSync(b), 'should have removed package from nm') + t.end() + }) +}) + +t.test('remove multiple installed libs', t => { + const path = t.testdir({ + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.0.0', + }), + }, + }, + 'package-lock.json': JSON.stringify({ + name: 'test-rm-single-lib', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'test-rm-single-lib', + version: '1.0.0', + dependencies: { + a: '*', + }, + }, + 'node_modules/a': { + version: '1.0.0', + }, + 'node_modules/b': { + version: '1.0.0', + }, + }, + dependencies: { + a: { + version: '1.0.0', + }, + b: { + version: '1.0.0', + }, + }, + }), + }) + + const a = resolve(path, 'node_modules/a') + const b = resolve(path, 'node_modules/b') + t.ok(() => fs.statSync(a)) + t.ok(() => fs.statSync(b)) + + npm.flatOptions.prefix = path + + uninstall(['b'], err => { + if (err) + throw err + + t.throws(() => fs.statSync(a), 'should have removed a package from nm') + t.throws(() => fs.statSync(b), 'should have removed b package from nm') + t.end() + }) +}) + +t.test('no args local', t => { + const path = t.testdir() + + npm.flatOptions.prefix = path + + uninstall([], err => { + t.match( + err, + /Must provide a package name to remove/, + 'should throw package name required error' + ) + + t.end() + }) +}) + +t.test('no args global', t => { + const path = t.testdir({ + lib: { + node_modules: { + a: t.fixture('symlink', '../../projects/a'), + }, + }, + projects: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + }), + }, + }, + }) + + npm.localPrefix = resolve(path, 'projects', 'a') + npm.globalDir = resolve(path, 'lib', 'node_modules') + npm.flatOptions.global = true + npm.flatOptions.prefix = path + + const a = resolve(path, 'lib/node_modules/a') + t.ok(() => fs.statSync(a)) + + uninstall([], err => { + if (err) + throw err + + t.throws(() => fs.statSync(a), 'should have removed global nm symlink') + + t.end() + }) +}) + +t.test('no args global but no package.json', t => { + const path = t.testdir({}) + + npm.prefix = path + npm.localPrefix = path + npm.flatOptions.global = true + + uninstall([], err => { + t.match( + err, + 'usage instructions', + 'should throw usage instructions' + ) + + t.end() + }) +}) + +t.test('unknown error reading from localPrefix package.json', t => { + const path = t.testdir({}) + + const uninstall = requireInject('../../lib/uninstall.js', { + ...mocks, + 'read-package-json-fast': () => Promise.reject(new Error('ERR')), + }) + + npm.prefix = path + npm.localPrefix = path + npm.flatOptions.global = true + + uninstall([], err => { + t.match( + err, + /ERR/, + 'should throw unknown error' + ) + + t.end() + }) +})