diff --git a/docs/content/commands/npm-dist-tag.md b/docs/content/commands/npm-dist-tag.md index 585da16ad2d2c..158c3417e7cba 100644 --- a/docs/content/commands/npm-dist-tag.md +++ b/docs/content/commands/npm-dist-tag.md @@ -88,6 +88,18 @@ semver as `>=1.4.0 <1.5.0`. See . The simplest way to avoid semver problems with tags is to use tags that do not begin with a number or the letter `v`. +### Configuration + +#### workspaces + +Only supported by `ls`. Enables listing dist-tags of all workspace +contexts defined in the current `package.json`. + +#### workspace + +Only supported by `ls`. Enables listing dist-tags of workspace contexts +limiting results to only those specified by this config item. + ### See Also * [npm publish](/commands/npm-publish) diff --git a/lib/dist-tag.js b/lib/dist-tag.js index 13ec37fd8cb1d..64e8abc013745 100644 --- a/lib/dist-tag.js +++ b/lib/dist-tag.js @@ -5,6 +5,7 @@ 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 { @@ -12,6 +13,11 @@ class DistTag extends BaseCommand { 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' @@ -43,15 +49,14 @@ class DistTag extends BaseCommand { async distTag ([cmdName, pkg, tag]) { const opts = this.npm.flatOptions - const has = (items) => new Set(items).has(cmdName) - if (has(['add', 'a', 'set', 's'])) + if (['add', 'a', 'set', 's'].includes(cmdName)) return this.add(pkg, tag, opts) - if (has(['rm', 'r', 'del', 'd', 'remove'])) + if (['rm', 'r', 'del', 'd', 'remove'].includes(cmdName)) return this.remove(pkg, tag, opts) - if (has(['ls', 'l', 'sl', 'list'])) + if (['ls', 'l', 'sl', 'list'].includes(cmdName)) return this.list(pkg, opts) if (!pkg) { @@ -62,6 +67,33 @@ class DistTag extends BaseCommand { 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 @@ -145,6 +177,22 @@ class DistTag extends BaseCommand { } } + 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`, diff --git a/tap-snapshots/test-lib-dist-tag.js-TAP.test.js b/tap-snapshots/test-lib-dist-tag.js-TAP.test.js index 06936795bcf03..ea25b568b0662 100644 --- a/tap-snapshots/test-lib-dist-tag.js-TAP.test.js +++ b/tap-snapshots/test-lib-dist-tag.js-TAP.test.js @@ -15,6 +15,9 @@ npm dist-tag add @ [] npm dist-tag rm npm dist-tag ls [] +Options: +[-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] + alias: dist-tags Run "npm help dist-tag" for more info @@ -30,6 +33,9 @@ npm dist-tag add @ [] npm dist-tag rm npm dist-tag ls [] +Options: +[-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] + alias: dist-tags Run "npm help dist-tag" for more info @@ -54,6 +60,9 @@ npm dist-tag add @ [] npm dist-tag rm npm dist-tag ls [] +Options: +[-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] + alias: dist-tags Run "npm help dist-tag" for more info @@ -75,6 +84,9 @@ npm dist-tag add @ [] npm dist-tag rm npm dist-tag ls [] +Options: +[-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] + alias: dist-tags Run "npm help dist-tag" for more info @@ -126,6 +138,9 @@ npm dist-tag add @ [] npm dist-tag rm npm dist-tag ls [] +Options: +[-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] + alias: dist-tags Run "npm help dist-tag" for more info @@ -142,3 +157,100 @@ dist-tag add b to @scoped/another@0.6.0 dist-tag add b is already set to version 0.6.0 ` + +exports[`test/lib/dist-tag.js TAP workspaces no args > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +workspace-b: +latest-b: 2.0.0 +latest: 2.0.0 +workspace-c: +latest-c: 3.0.0 +latest: 3.0.0 +` + +exports[`test/lib/dist-tag.js TAP workspaces no args, one failing workspace sets exitCode to 1 > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +workspace-b: +latest-b: 2.0.0 +latest: 2.0.0 +workspace-c: +latest-c: 3.0.0 +latest: 3.0.0 +workspace-d: +` + +exports[`test/lib/dist-tag.js TAP workspaces no args, one workspace > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +` + +exports[`test/lib/dist-tag.js TAP workspaces one arg -- . > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +workspace-b: +latest-b: 2.0.0 +latest: 2.0.0 +workspace-c: +latest-c: 3.0.0 +latest: 3.0.0 +` + +exports[`test/lib/dist-tag.js TAP workspaces one arg -- .@1, ignores version spec > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +workspace-b: +latest-b: 2.0.0 +latest: 2.0.0 +workspace-c: +latest-c: 3.0.0 +latest: 3.0.0 +` + +exports[`test/lib/dist-tag.js TAP workspaces one arg -- list > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +workspace-b: +latest-b: 2.0.0 +latest: 2.0.0 +workspace-c: +latest-c: 3.0.0 +latest: 3.0.0 +` + +exports[`test/lib/dist-tag.js TAP workspaces two args -- list, . > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +workspace-b: +latest-b: 2.0.0 +latest: 2.0.0 +workspace-c: +latest-c: 3.0.0 +latest: 3.0.0 +` + +exports[`test/lib/dist-tag.js TAP workspaces two args -- list, .@1, ignores version spec > printed the expected output 1`] = ` +workspace-a: +latest-a: 1.0.0 +latest: 1.0.0 +workspace-b: +latest-b: 2.0.0 +latest: 2.0.0 +workspace-c: +latest-c: 3.0.0 +latest: 3.0.0 +` + +exports[`test/lib/dist-tag.js TAP workspaces two args -- list, @scoped/pkg, logs a warning and ignores workspaces > printed the expected output 1`] = ` +a: 0.0.1 +b: 0.5.0 +latest: 1.0.0 +` diff --git a/tap-snapshots/test-lib-utils-npm-usage.js-TAP.test.js b/tap-snapshots/test-lib-utils-npm-usage.js-TAP.test.js index 45863cdf4e02f..19beaaa85ea48 100644 --- a/tap-snapshots/test-lib-utils-npm-usage.js-TAP.test.js +++ b/tap-snapshots/test-lib-utils-npm-usage.js-TAP.test.js @@ -323,6 +323,9 @@ All commands: npm dist-tag rm npm dist-tag ls [] + Options: + [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] + alias: dist-tags Run "npm help dist-tag" for more info diff --git a/test/lib/dist-tag.js b/test/lib/dist-tag.js index 9415dacbe4756..5e54c8f991cfd 100644 --- a/test/lib/dist-tag.js +++ b/test/lib/dist-tag.js @@ -1,10 +1,16 @@ const requireInject = require('require-inject') const mockNpm = require('../fixtures/mock-npm') -const { test } = require('tap') +const { afterEach, test } = require('tap') let result = '' let log = '' +afterEach((cb) => { + result = '' + log = '' + cb() +}) + const routeMap = { '/-/package/@scoped%2fpkg/dist-tags': { latest: '1.0.0', @@ -22,6 +28,18 @@ const routeMap = { b: '0.6.0', c: '7.7.7', }, + '/-/package/workspace-a/dist-tags': { + latest: '1.0.0', + 'latest-a': '1.0.0', + }, + '/-/package/workspace-b/dist-tags': { + latest: '2.0.0', + 'latest-b': '2.0.0', + }, + '/-/package/workspace-c/dist-tags': { + latest: '3.0.0', + 'latest-c': '3.0.0', + }, } let npmRegistryFetchMock = (url, opts) => { @@ -57,7 +75,7 @@ const npm = mockNpm({ global: false, }, output: msg => { - result = msg + result = result ? [result, msg].join('\n') : msg }, }) const distTag = new DistTag(npm) @@ -74,8 +92,6 @@ test('ls in current package', (t) => { result, 'should list available tags for current package' ) - result = '' - log = '' t.end() }) }) @@ -92,8 +108,6 @@ test('no args in current package', (t) => { result, 'should default to listing available tags for current package' ) - result = '' - log = '' t.end() }) }) @@ -102,8 +116,6 @@ test('borked cmd usage', (t) => { npm.prefix = t.testdir({}) distTag.exec(['borked', '@scoped/pkg'], (err) => { t.matchSnapshot(err, 'should show usage error') - result = '' - log = '' t.end() }) }) @@ -116,8 +128,6 @@ test('ls on named package', (t) => { result, 'should list tags for the specified package' ) - result = '' - log = '' t.end() }) }) @@ -133,8 +143,6 @@ test('ls on missing package', (t) => { err, 'should throw error message' ) - result = '' - log = '' t.end() }) }) @@ -150,8 +158,6 @@ test('ls on missing name in current package', (t) => { err, 'should throw usage error message' ) - result = '' - log = '' t.end() }) }) @@ -164,14 +170,154 @@ test('only named package arg', (t) => { result, 'should default to listing tags for the specified package' ) - result = '' - log = '' t.end() }) }) +test('workspaces', (t) => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b', 'workspace-c'], + }), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.0.0', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.0.0', + }), + }, + 'workspace-c': { + 'package.json': JSON.stringify({ + name: 'workspace-c', + version: '1.0.0', + }), + }, + }) + + t.test('no args', t => { + distTag.execWorkspaces([], [], (err) => { + t.ifError(err) + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('no args, one workspace', t => { + distTag.execWorkspaces([], ['workspace-a'], (err) => { + t.ifError(err) + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('one arg -- .', t => { + distTag.execWorkspaces(['.'], [], (err) => { + t.ifError(err) + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('one arg -- .@1, ignores version spec', t => { + distTag.execWorkspaces(['.@'], [], (err) => { + t.ifError(err) + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('one arg -- list', t => { + distTag.execWorkspaces(['list'], [], (err) => { + t.ifError(err) + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('two args -- list, .', t => { + distTag.execWorkspaces(['list', '.'], [], (err) => { + t.ifError(err) + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('two args -- list, .@1, ignores version spec', t => { + distTag.execWorkspaces(['list', '.@'], [], (err) => { + t.ifError(err) + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('two args -- list, @scoped/pkg, logs a warning and ignores workspaces', t => { + distTag.execWorkspaces(['list', '@scoped/pkg'], [], (err) => { + t.ifError(err) + t.match(log, 'Ignoring workspaces for specified package', 'logs a warning') + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.test('no args, one failing workspace sets exitCode to 1', t => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b', 'workspace-c', 'workspace-d'], + }), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.0.0', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.0.0', + }), + }, + 'workspace-c': { + 'package.json': JSON.stringify({ + name: 'workspace-c', + version: '1.0.0', + }), + }, + 'workspace-d': { + 'package.json': JSON.stringify({ + name: 'workspace-d', + version: '1.0.0', + }), + }, + }) + + distTag.execWorkspaces([], [], (err) => { + t.ifError(err) + t.equal(process.exitCode, 1, 'set the error status') + process.exitCode = 0 + t.match(log, 'dist-tag ls Couldn\'t get dist-tag data for workspace-d@latest', 'logs the error') + t.matchSnapshot(result, 'printed the expected output') + t.end() + }) + }) + + t.end() +}) + test('add new tag', (t) => { const _nrf = npmRegistryFetchMock + t.teardown(() => { + npmRegistryFetchMock = _nrf + }) + npmRegistryFetchMock = async (url, opts) => { t.equal(opts.method, 'PUT', 'should trigger request to add new tag') t.equal(opts.body, '7.7.7', 'should point to expected version') @@ -183,9 +329,6 @@ test('add new tag', (t) => { result, 'should return success msg' ) - result = '' - log = '' - npmRegistryFetchMock = _nrf t.end() }) }) @@ -202,8 +345,6 @@ test('add using valid semver range as name', (t) => { log, 'should return success msg' ) - result = '' - log = '' t.end() }) }) @@ -212,8 +353,6 @@ test('add missing args', (t) => { npm.prefix = t.testdir({}) distTag.exec(['add', '@scoped/another@7.7.7'], (err) => { t.matchSnapshot(err, 'should exit usage error message') - result = '' - log = '' t.end() }) }) @@ -222,8 +361,6 @@ test('add missing pkg name', (t) => { npm.prefix = t.testdir({}) distTag.exec(['add', null], (err) => { t.matchSnapshot(err, 'should exit usage error message') - result = '' - log = '' t.end() }) }) @@ -236,13 +373,16 @@ test('set existing version', (t) => { log, 'should log warn msg' ) - log = '' t.end() }) }) test('remove existing tag', (t) => { const _nrf = npmRegistryFetchMock + t.teardown(() => { + npmRegistryFetchMock = _nrf + }) + npmRegistryFetchMock = async (url, opts) => { t.equal(opts.method, 'DELETE', 'should trigger request to remove tag') } @@ -251,9 +391,6 @@ test('remove existing tag', (t) => { t.ifError(err, 'npm dist-tags rm') t.matchSnapshot(log, 'should log remove info') t.matchSnapshot(result, 'should return success msg') - result = '' - log = '' - npmRegistryFetchMock = _nrf t.end() }) }) @@ -267,8 +404,6 @@ test('remove non-existing tag', (t) => { 'should exit with error' ) t.matchSnapshot(log, 'should log error msg') - result = '' - log = '' t.end() }) }) @@ -277,8 +412,6 @@ test('remove missing pkg name', (t) => { npm.prefix = t.testdir({}) distTag.exec(['rm', null], (err) => { t.matchSnapshot(err, 'should exit usage error message') - result = '' - log = '' t.end() }) })