1'use strict' 2 3const { URL } = require('url') 4const npa = require('npm-package-arg') 5const npmFetch = require('npm-registry-fetch') 6const semver = require('semver') 7 8// given a tarball url and a registry url, returns just the 9// relevant pathname portion of it, so that it can be handled 10// elegantly by npm-registry-fetch which only expects pathnames 11// and handles the registry hostname via opts 12const getPathname = (tarball, registry) => { 13 const registryUrl = new URL(registry).pathname.slice(1) 14 let tarballUrl = new URL(tarball).pathname.slice(1) 15 16 // test the tarball url to see if it starts with a possible 17 // pathname from the registry url, in that case strips that portion 18 // of it so that we only return the post-registry-url pathname 19 if (registryUrl) { 20 tarballUrl = tarballUrl.slice(registryUrl.length) 21 } 22 return tarballUrl 23} 24 25const unpublish = async (spec, opts) => { 26 spec = npa(spec) 27 // spec is used to pick the appropriate registry/auth combo. 28 opts = { 29 force: false, 30 ...opts, 31 spec, 32 } 33 34 try { 35 const pkgUri = spec.escapedName 36 const pkg = await npmFetch.json(pkgUri, { 37 ...opts, 38 query: { write: true }, 39 }) 40 41 const version = spec.rawSpec 42 const allVersions = pkg.versions || {} 43 const versionData = allVersions[version] 44 45 const rawSpecs = (!spec.rawSpec || spec.rawSpec === '*') 46 const onlyVersion = Object.keys(allVersions).length === 1 47 const noVersions = !Object.keys(allVersions).length 48 49 // if missing specific version, 50 // assumed unpublished 51 if (!versionData && !rawSpecs && !noVersions) { 52 return true 53 } 54 55 // unpublish all versions of a package: 56 // - no specs supplied "npm unpublish foo" 57 // - all specs ("*") "npm unpublish foo@*" 58 // - there was only one version 59 // - has no versions field on packument 60 if (rawSpecs || onlyVersion || noVersions) { 61 await npmFetch(`${pkgUri}/-rev/${pkg._rev}`, { 62 ...opts, 63 method: 'DELETE', 64 ignoreBody: true, 65 }) 66 return true 67 } else { 68 const dist = allVersions[version].dist 69 delete allVersions[version] 70 71 const latestVer = pkg['dist-tags'].latest 72 73 // deleting dist tags associated to version 74 Object.keys(pkg['dist-tags']).forEach(tag => { 75 if (pkg['dist-tags'][tag] === version) { 76 delete pkg['dist-tags'][tag] 77 } 78 }) 79 80 if (latestVer === version) { 81 pkg['dist-tags'].latest = Object.keys( 82 allVersions 83 ).sort(semver.compareLoose).pop() 84 } 85 86 delete pkg._revisions 87 delete pkg._attachments 88 89 // Update packument with removed versions 90 await npmFetch(`${pkgUri}/-rev/${pkg._rev}`, { 91 ...opts, 92 method: 'PUT', 93 body: pkg, 94 ignoreBody: true, 95 }) 96 97 // Remove the tarball itself 98 const { _rev } = await npmFetch.json(pkgUri, { 99 ...opts, 100 query: { write: true }, 101 }) 102 const tarballUrl = getPathname(dist.tarball, opts.registry) 103 await npmFetch(`${tarballUrl}/-rev/${_rev}`, { 104 ...opts, 105 method: 'DELETE', 106 ignoreBody: true, 107 }) 108 return true 109 } 110 } catch (err) { 111 if (err.code !== 'E404') { 112 throw err 113 } 114 115 return true 116 } 117} 118 119module.exports = unpublish 120