1const libaccess = require('libnpmaccess') 2const libunpub = require('libnpmpublish').unpublish 3const npa = require('npm-package-arg') 4const pacote = require('pacote') 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 packument = await pacote.packument(name, { 27 ...opts, 28 spec: name, 29 query: { write: true }, 30 }) 31 return Object.keys(packument.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 Unpublish.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, { localPrefix } = {}) { 71 if (args.length > 1) { 72 throw this.usageError() 73 } 74 75 // workspace mode 76 if (!localPrefix) { 77 localPrefix = this.npm.localPrefix 78 } 79 80 const force = this.npm.config.get('force') 81 const { silent } = this.npm 82 const dryRun = this.npm.config.get('dry-run') 83 84 let spec 85 if (args.length) { 86 spec = npa(args[0]) 87 if (spec.type !== 'version' && spec.rawSpec !== '*') { 88 throw this.usageError( 89 'Can only unpublish a single version, or the entire project.\n' + 90 'Tags and ranges are not supported.' 91 ) 92 } 93 } 94 95 log.silly('unpublish', 'args[0]', args[0]) 96 log.silly('unpublish', 'spec', spec) 97 98 if (spec?.rawSpec === '*' && !force) { 99 throw this.usageError( 100 'Refusing to delete entire project.\n' + 101 'Run with --force to do this.' 102 ) 103 } 104 105 const opts = { ...this.npm.flatOptions } 106 107 let manifest 108 try { 109 const { content } = await pkgJson.prepare(localPrefix) 110 manifest = content 111 } catch (err) { 112 if (err.code === 'ENOENT' || err.code === 'ENOTDIR') { 113 if (!spec) { 114 // We needed a local package.json to figure out what package to 115 // unpublish 116 throw this.usageError() 117 } 118 } else { 119 // folks should know if ANY local package.json had a parsing error. 120 // They may be relying on `publishConfig` to be loading and we don't 121 // want to ignore errors in that case. 122 throw err 123 } 124 } 125 126 let pkgVersion // for cli output 127 if (spec) { 128 pkgVersion = spec.type === 'version' ? `@${spec.rawSpec}` : '' 129 } else { 130 spec = npa.resolve(manifest.name, manifest.version) 131 log.verbose('unpublish', manifest) 132 pkgVersion = manifest.version ? `@${manifest.version}` : '' 133 if (!manifest.version && !force) { 134 throw this.usageError( 135 'Refusing to delete entire project.\n' + 136 'Run with --force to do this.' 137 ) 138 } 139 } 140 141 // If localPrefix has a package.json with a name that matches the package 142 // being unpublished, load up the publishConfig 143 if (manifest?.name === spec.name && manifest.publishConfig) { 144 flatten(manifest.publishConfig, opts) 145 } 146 147 const versions = await Unpublish.getKeysOfVersions(spec.name, opts) 148 if (versions.length === 1 && spec.rawSpec === versions[0] && !force) { 149 throw this.usageError(LAST_REMAINING_VERSION_ERROR) 150 } 151 if (versions.length === 1) { 152 pkgVersion = '' 153 } 154 155 if (!dryRun) { 156 await otplease(this.npm, opts, o => libunpub(spec, o)) 157 } 158 if (!silent) { 159 this.npm.output(`- ${spec.name}${pkgVersion}`) 160 } 161 } 162 163 async execWorkspaces (args) { 164 await this.setWorkspaces() 165 166 for (const path of this.workspacePaths) { 167 await this.exec(args, { localPrefix: path }) 168 } 169 } 170} 171module.exports = Unpublish 172