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