unpublish.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. const libaccess = require('libnpmaccess')
  2. const libunpub = require('libnpmpublish').unpublish
  3. const npa = require('npm-package-arg')
  4. const pacote = require('pacote')
  5. const { output, log } = require('proc-log')
  6. const pkgJson = require('@npmcli/package-json')
  7. const { flatten } = require('@npmcli/config/lib/definitions')
  8. const getIdentity = require('../utils/get-identity.js')
  9. const { otplease } = require('../utils/auth.js')
  10. const BaseCommand = require('../base-cmd.js')
  11. const LAST_REMAINING_VERSION_ERROR = 'Refusing to delete the last version of the package. It will block from republishing a new version for 24 hours.\nRun with --force to do this.'
  12. class Unpublish extends BaseCommand {
  13. static description = 'Remove a package from the registry'
  14. static name = 'unpublish'
  15. static params = ['dry-run', 'force', 'workspace', 'workspaces']
  16. static usage = ['[<package-spec>]']
  17. static workspaces = true
  18. static ignoreImplicitWorkspace = false
  19. static async getKeysOfVersions (name, opts) {
  20. const packument = await pacote.packument(name, {
  21. ...opts,
  22. spec: name,
  23. query: { write: true },
  24. _isRoot: true,
  25. })
  26. return Object.keys(packument.versions)
  27. }
  28. static async completion (args, npm) {
  29. const { partialWord, conf } = args
  30. if (conf.argv.remain.length >= 3) {
  31. return []
  32. }
  33. const opts = { ...npm.flatOptions }
  34. const username = await getIdentity(npm, { ...opts }).catch(() => null)
  35. if (!username) {
  36. return []
  37. }
  38. const access = await libaccess.getPackages(username, opts)
  39. // do a bit of filtering at this point, so that we don't need to fetch versions for more than one thing, but also don't accidentally unpublish a whole project
  40. let pkgs = Object.keys(access)
  41. if (!partialWord || !pkgs.length) {
  42. return pkgs
  43. }
  44. const pp = npa(partialWord).name
  45. pkgs = pkgs.filter(p => !p.indexOf(pp))
  46. if (pkgs.length > 1) {
  47. return pkgs
  48. }
  49. const versions = await Unpublish.getKeysOfVersions(pkgs[0], opts)
  50. if (!versions.length) {
  51. return pkgs
  52. } else {
  53. return versions.map(v => `${pkgs[0]}@${v}`)
  54. }
  55. }
  56. async exec (args, { localPrefix } = {}) {
  57. if (args.length > 1) {
  58. throw this.usageError()
  59. }
  60. // workspace mode
  61. if (!localPrefix) {
  62. localPrefix = this.npm.localPrefix
  63. }
  64. const force = this.npm.config.get('force')
  65. const { silent } = this.npm
  66. const dryRun = this.npm.config.get('dry-run')
  67. let spec
  68. if (args.length) {
  69. spec = npa(args[0])
  70. if (spec.type !== 'version' && spec.rawSpec !== '*') {
  71. throw this.usageError('Can only unpublish a single version, or the entire project.\nTags and ranges are not supported.')
  72. }
  73. }
  74. log.silly('unpublish', 'args[0]', args[0])
  75. log.silly('unpublish', 'spec', spec)
  76. if (spec?.rawSpec === '*' && !force) {
  77. throw this.usageError('Refusing to delete entire project.\nRun with --force to do this.')
  78. }
  79. const opts = { ...this.npm.flatOptions }
  80. let manifest
  81. try {
  82. const { content } = await pkgJson.prepare(localPrefix)
  83. manifest = content
  84. } catch (err) {
  85. if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
  86. if (!spec) {
  87. // We needed a local package.json to figure out what package to unpublish
  88. throw this.usageError()
  89. }
  90. } else {
  91. // folks should know if ANY local package.json had a parsing error.
  92. // They may be relying on `publishConfig` to be loading and we don't want to ignore errors in that case.
  93. throw err
  94. }
  95. }
  96. let pkgVersion // for cli output
  97. if (spec) {
  98. pkgVersion = spec.type === 'version' ? `@${spec.rawSpec}` : ''
  99. } else {
  100. spec = npa.resolve(manifest.name, manifest.version)
  101. log.verbose('unpublish', manifest)
  102. pkgVersion = manifest.version ? `@${manifest.version}` : ''
  103. if (!manifest.version && !force) {
  104. throw this.usageError('Refusing to delete entire project.\nRun with --force to do this.')
  105. }
  106. }
  107. // If localPrefix has a package.json with a name that matches the package being unpublished, load up the publishConfig
  108. if (manifest?.name === spec.name && manifest.publishConfig) {
  109. const cliFlags = this.npm.config.data.get('cli').raw
  110. // Filter out properties set in CLI flags to prioritize them over corresponding `publishConfig` settings
  111. const filteredPublishConfig = Object.fromEntries(
  112. Object.entries(manifest.publishConfig).filter(([key]) => !(key in cliFlags)))
  113. for (const key in filteredPublishConfig) {
  114. this.npm.config.checkUnknown('publishConfig', key)
  115. }
  116. flatten(filteredPublishConfig, opts)
  117. }
  118. const versions = await Unpublish.getKeysOfVersions(spec.name, opts)
  119. if (versions.length === 1 && spec.rawSpec === versions[0] && !force) {
  120. throw this.usageError(LAST_REMAINING_VERSION_ERROR)
  121. }
  122. if (versions.length === 1) {
  123. pkgVersion = ''
  124. }
  125. if (!dryRun) {
  126. await otplease(this.npm, opts, o => libunpub(spec, o))
  127. }
  128. if (!silent) {
  129. output.standard(`- ${spec.name}${pkgVersion}`)
  130. }
  131. }
  132. async execWorkspaces (args) {
  133. await this.setWorkspaces()
  134. for (const path of this.workspacePaths) {
  135. await this.exec(args, { localPrefix: path })
  136. }
  137. }
  138. }
  139. module.exports = Unpublish