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