• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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