const log = require('npmlog') const npa = require('npm-package-arg') const regFetch = require('npm-registry-fetch') const semver = require('semver') const otplease = require('./utils/otplease.js') const readLocalPkgName = require('./utils/read-local-package.js') const getWorkspaces = require('./workspaces/get-workspaces.js') const BaseCommand = require('./base-command.js') class DistTag extends BaseCommand { static get description () { return 'Modify package distribution tags' } /* istanbul ignore next - see test/lib/load-all-commands.js */ static get params () { return ['workspace', 'workspaces'] } /* istanbul ignore next - see test/lib/load-all-commands.js */ static get name () { return 'dist-tag' } /* istanbul ignore next - see test/lib/load-all-commands.js */ static get usage () { return [ 'add @ []', 'rm ', 'ls []', ] } async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) return ['add', 'rm', 'ls'] switch (argv[2]) { default: return [] } } exec (args, cb) { this.distTag(args).then(() => cb()).catch(cb) } async distTag ([cmdName, pkg, tag]) { const opts = this.npm.flatOptions if (['add', 'a', 'set', 's'].includes(cmdName)) return this.add(pkg, tag, opts) if (['rm', 'r', 'del', 'd', 'remove'].includes(cmdName)) return this.remove(pkg, tag, opts) if (['ls', 'l', 'sl', 'list'].includes(cmdName)) return this.list(pkg, opts) if (!pkg) { // when only using the pkg name the default behavior // should be listing the existing tags return this.list(cmdName, opts) } else throw this.usage } execWorkspaces (args, filters, cb) { this.distTagWorkspaces(args, filters).then(() => cb()).catch(cb) } async distTagWorkspaces ([cmdName, pkg, tag], filters) { // cmdName is some form of list // pkg is one of: // - unset // - . // - .@version if (['ls', 'l', 'sl', 'list'].includes(cmdName) && (!pkg || pkg === '.' || /^\.@/.test(pkg))) return this.listWorkspaces(filters) // pkg is unset // cmdName is one of: // - unset // - . // - .@version if (!pkg && (!cmdName || cmdName === '.' || /^\.@/.test(cmdName))) return this.listWorkspaces(filters) // anything else is just a regular dist-tag command // so we fallback to the non-workspaces implementation log.warn('Ignoring workspaces for specified package') return this.distTag([cmdName, pkg, tag]) } async add (spec, tag, opts) { spec = npa(spec || '') const version = spec.rawSpec const defaultTag = tag || this.npm.config.get('tag') log.verbose('dist-tag add', defaultTag, 'to', spec.name + '@' + version) if (!spec.name || !version || !defaultTag) throw this.usage const t = defaultTag.trim() if (semver.validRange(t)) throw new Error('Tag name must not be a valid SemVer range: ' + t) const tags = await this.fetchTags(spec, opts) if (tags[t] === version) { log.warn('dist-tag add', t, 'is already set to version', version) return } tags[t] = version const url = `/-/package/${spec.escapedName}/dist-tags/${encodeURIComponent(t)}` const reqOpts = { ...opts, method: 'PUT', body: JSON.stringify(version), headers: { 'content-type': 'application/json', }, spec, } await otplease(reqOpts, reqOpts => regFetch(url, reqOpts)) this.npm.output(`+${t}: ${spec.name}@${version}`) } async remove (spec, tag, opts) { spec = npa(spec || '') log.verbose('dist-tag del', tag, 'from', spec.name) if (!spec.name) throw this.usage const tags = await this.fetchTags(spec, opts) if (!tags[tag]) { log.info('dist-tag del', tag, 'is not a dist-tag on', spec.name) throw new Error(tag + ' is not a dist-tag on ' + spec.name) } const version = tags[tag] delete tags[tag] const url = `/-/package/${spec.escapedName}/dist-tags/${encodeURIComponent(tag)}` const reqOpts = { ...opts, method: 'DELETE', spec, } await otplease(reqOpts, reqOpts => regFetch(url, reqOpts)) this.npm.output(`-${tag}: ${spec.name}@${version}`) } async list (spec, opts) { if (!spec) { const pkg = await readLocalPkgName(this.npm) if (!pkg) throw this.usage return this.list(pkg, opts) } spec = npa(spec) try { const tags = await this.fetchTags(spec, opts) const msg = Object.keys(tags).map(k => `${k}: ${tags[k]}`).sort().join('\n') this.npm.output(msg) return tags } catch (err) { log.error('dist-tag ls', "Couldn't get dist-tag data for", spec) throw err } } async listWorkspaces (filters) { const workspaces = await getWorkspaces(filters, { path: this.npm.localPrefix }) for (const [name] of workspaces) { try { this.npm.output(`${name}:`) await this.list(npa(name), this.npm.flatOptions) } catch (err) { // set the exitCode directly, but ignore the error // since it will have already been logged by this.list() process.exitCode = 1 } } } async fetchTags (spec, opts) { const data = await regFetch.json( `/-/package/${spec.escapedName}/dist-tags`, { ...opts, 'prefer-online': true, spec } ) if (data && typeof data === 'object') delete data._etag if (!data || !Object.keys(data).length) throw new Error('No dist-tags found for ' + spec.name) return data } } module.exports = DistTag