1const libaccess = require('libnpmaccess') 2const libunpub = require('libnpmpublish').unpublish 3const npa = require('npm-package-arg') 4const npmFetch = require('npm-registry-fetch') 5const pkgJson = require('@npmcli/package-json') 6 7const { flatten } = require('@npmcli/config/lib/definitions') 8const getIdentity = require('../utils/get-identity.js') 9const log = require('../utils/log-shim') 10const otplease = require('../utils/otplease.js') 11 12const LAST_REMAINING_VERSION_ERROR = 'Refusing to delete the last version of the package. ' + 13'It will block from republishing a new version for 24 hours.\n' + 14'Run with --force to do this.' 15 16const BaseCommand = require('../base-command.js') 17class Unpublish extends BaseCommand { 18 static description = 'Remove a package from the registry' 19 static name = 'unpublish' 20 static params = ['dry-run', 'force', 'workspace', 'workspaces'] 21 static usage = ['[<package-spec>]'] 22 static workspaces = true 23 static ignoreImplicitWorkspace = false 24 25 static async getKeysOfVersions (name, opts) { 26 const pkgUri = npa(name).escapedName 27 const json = await npmFetch.json(`${pkgUri}?write=true`, { 28 ...opts, 29 spec: name, 30 }) 31 return Object.keys(json.versions) 32 } 33 34 static async completion (args, npm) { 35 const { partialWord, conf } = args 36 37 if (conf.argv.remain.length >= 3) { 38 return [] 39 } 40 41 const opts = { ...npm.flatOptions } 42 const username = await getIdentity(npm, { ...opts }).catch(() => null) 43 if (!username) { 44 return [] 45 } 46 47 const access = await libaccess.getPackages(username, opts) 48 // do a bit of filtering at this point, so that we don't need 49 // to fetch versions for more than one thing, but also don't 50 // accidentally unpublish a whole project 51 let pkgs = Object.keys(access) 52 if (!partialWord || !pkgs.length) { 53 return pkgs 54 } 55 56 const pp = npa(partialWord).name 57 pkgs = pkgs.filter(p => !p.indexOf(pp)) 58 if (pkgs.length > 1) { 59 return pkgs 60 } 61 62 const versions = await this.getKeysOfVersions(pkgs[0], opts) 63 if (!versions.length) { 64 return pkgs 65 } else { 66 return versions.map(v => `${pkgs[0]}@${v}`) 67 } 68 } 69 70 async exec (args) { 71 if (args.length > 1) { 72 throw this.usageError() 73 } 74 75 let spec = args.length && npa(args[0]) 76 const force = this.npm.config.get('force') 77 const { silent } = this.npm 78 const dryRun = this.npm.config.get('dry-run') 79 80 log.silly('unpublish', 'args[0]', args[0]) 81 log.silly('unpublish', 'spec', spec) 82 83 if ((!spec || !spec.rawSpec) && !force) { 84 throw this.usageError( 85 'Refusing to delete entire project.\n' + 86 'Run with --force to do this.' 87 ) 88 } 89 90 const opts = { ...this.npm.flatOptions } 91 92 let pkgName 93 let pkgVersion 94 let manifest 95 let manifestErr 96 try { 97 const { content } = await pkgJson.prepare(this.npm.localPrefix) 98 manifest = content 99 } catch (err) { 100 manifestErr = err 101 } 102 if (spec) { 103 // If cwd has a package.json with a name that matches the package being 104 // unpublished, load up the publishConfig 105 if (manifest && manifest.name === spec.name && manifest.publishConfig) { 106 flatten(manifest.publishConfig, opts) 107 } 108 const versions = await Unpublish.getKeysOfVersions(spec.name, opts) 109 if (versions.length === 1 && !force) { 110 throw this.usageError(LAST_REMAINING_VERSION_ERROR) 111 } 112 pkgName = spec.name 113 pkgVersion = spec.type === 'version' ? `@${spec.rawSpec}` : '' 114 } else { 115 if (manifestErr) { 116 if (manifestErr.code === 'ENOENT' || manifestErr.code === 'ENOTDIR') { 117 throw this.usageError() 118 } else { 119 throw manifestErr 120 } 121 } 122 123 log.verbose('unpublish', manifest) 124 125 spec = npa.resolve(manifest.name, manifest.version) 126 if (manifest.publishConfig) { 127 flatten(manifest.publishConfig, opts) 128 } 129 130 pkgName = manifest.name 131 pkgVersion = manifest.version ? `@${manifest.version}` : '' 132 } 133 134 if (!dryRun) { 135 await otplease(this.npm, opts, o => libunpub(spec, o)) 136 } 137 if (!silent) { 138 this.npm.output(`- ${pkgName}${pkgVersion}`) 139 } 140 } 141 142 async execWorkspaces (args) { 143 await this.setWorkspaces() 144 145 const force = this.npm.config.get('force') 146 if (!force) { 147 throw this.usageError( 148 'Refusing to delete entire project(s).\n' + 149 'Run with --force to do this.' 150 ) 151 } 152 153 for (const name of this.workspaceNames) { 154 await this.exec([name]) 155 } 156 } 157} 158module.exports = Unpublish 159