From a1778d64d62e080cd9c8c9138c63bbf9ddbfd718 Mon Sep 17 00:00:00 2001 From: Gar Date: Fri, 3 Jan 2025 10:05:44 -0800 Subject: [PATCH 1/2] feat: add npm undeprecate command --- docs/lib/check-nav.js | 6 +- docs/lib/content/commands/npm-undeprecate.md | 24 +++++++ docs/lib/content/nav.yml | 3 + lib/commands/undeprecate.js | 13 ++++ lib/utils/cmd-list.js | 1 + .../tap-snapshots/test/index.js.test.cjs | 3 +- .../test/lib/commands/completion.js.test.cjs | 1 + .../test/lib/commands/publish.js.test.cjs | 1 + tap-snapshots/test/lib/docs.js.test.cjs | 26 ++++++- tap-snapshots/test/lib/npm.js.test.cjs | 52 ++++++++------ test/lib/commands/undeprecate.js | 72 +++++++++++++++++++ 11 files changed, 175 insertions(+), 27 deletions(-) create mode 100644 docs/lib/content/commands/npm-undeprecate.md create mode 100644 lib/commands/undeprecate.js create mode 100644 test/lib/commands/undeprecate.js diff --git a/docs/lib/check-nav.js b/docs/lib/check-nav.js index ac2c01038f438..0f9b3529c7546 100644 --- a/docs/lib/check-nav.js +++ b/docs/lib/check-nav.js @@ -29,17 +29,17 @@ function ensureNavigationComplete (nav, fsPaths, ext) { const errors = [] if (missingNav.length) { - errors.push('The following path(s) exist on disk but are not present in nav.yml:') + errors.push('The following path(s) exist on disk but are not present in /lib/content/nav.yml:') errors.push(...missingNav.map(n => ` ${n}`)) } if (missingFs.length) { - errors.push('The following path(s) exist in nav.yml but are not present on disk:') + errors.push('The following path(s) exist in lib/content/nav.yml but are not present on disk:') errors.push(...missingFs.map(n => ` ${n}`)) } if (errors.length) { - errors.unshift('Documentation navigation (nav.yml) does not match filesystem.') + errors.unshift('Documentation navigation (lib/content/nav.yml) does not match filesystem.') errors.push('Update nav.yml to ensure that all files are listed in the appropriate place.') throw new Error(errors.join('\n')) } diff --git a/docs/lib/content/commands/npm-undeprecate.md b/docs/lib/content/commands/npm-undeprecate.md new file mode 100644 index 0000000000000..076ac9eff2d0a --- /dev/null +++ b/docs/lib/content/commands/npm-undeprecate.md @@ -0,0 +1,24 @@ +--- +title: npm-undeprecate +section: 1 +description: Undeprecate a version of a package +--- + +### Synopsis + + + +### Description + +This command will update the npm registry entry for a package, removing any +deprecation warnings that currently exist. + +It works in the same way as [npm deprecate](/commands/npm-deprecate), except +that this command removes deprecation warnings instead of adding them. + +### Configuration + + +### See Also + +* [npm deprecate](/commands/npm-deprecate) diff --git a/docs/lib/content/nav.yml b/docs/lib/content/nav.yml index 96c89e5cc1b71..4148c4533efcb 100644 --- a/docs/lib/content/nav.yml +++ b/docs/lib/content/nav.yml @@ -177,6 +177,9 @@ - title: npm token url: /commands/npm-token description: Manage your authentication tokens + - title: npm undeprecate + url: /commands/npm-undeprecate + description: Undeprecate a version of a package - title: npm uninstall url: /commands/npm-uninstall description: Remove a package diff --git a/lib/commands/undeprecate.js b/lib/commands/undeprecate.js new file mode 100644 index 0000000000000..79ce66bbe5600 --- /dev/null +++ b/lib/commands/undeprecate.js @@ -0,0 +1,13 @@ +const Deprecate = require('./deprecate.js') + +class Undeprecate extends Deprecate { + static description = 'Undeprecate a version of a package' + static name = 'undeprecate' + static usage = [''] + + async exec ([pkg]) { + return super.exec([pkg, '']) + } +} + +module.exports = Undeprecate diff --git a/lib/utils/cmd-list.js b/lib/utils/cmd-list.js index 039d6ffddeb16..96eb0974a2ed3 100644 --- a/lib/utils/cmd-list.js +++ b/lib/utils/cmd-list.js @@ -62,6 +62,7 @@ const commands = [ 'team', 'test', 'token', + 'undeprecate', 'uninstall', 'unpublish', 'unstar', diff --git a/smoke-tests/tap-snapshots/test/index.js.test.cjs b/smoke-tests/tap-snapshots/test/index.js.test.cjs index 1de04263b4942..c04e01166f7cb 100644 --- a/smoke-tests/tap-snapshots/test/index.js.test.cjs +++ b/smoke-tests/tap-snapshots/test/index.js.test.cjs @@ -29,7 +29,8 @@ All commands: ping, pkg, prefix, profile, prune, publish, query, rebuild, repo, restart, root, run-script, sbom, search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, unstar, update, version, view, whoami + undeprecate, uninstall, unpublish, unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {NPM}/{TESTDIR}/home/.npmrc diff --git a/tap-snapshots/test/lib/commands/completion.js.test.cjs b/tap-snapshots/test/lib/commands/completion.js.test.cjs index 9b6f1ba51c787..a281883539f61 100644 --- a/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -100,6 +100,7 @@ Array [ team test token + undeprecate uninstall unpublish unstar diff --git a/tap-snapshots/test/lib/commands/publish.js.test.cjs b/tap-snapshots/test/lib/commands/publish.js.test.cjs index c0dc06b568180..1599f1dd5c394 100644 --- a/tap-snapshots/test/lib/commands/publish.js.test.cjs +++ b/tap-snapshots/test/lib/commands/publish.js.test.cjs @@ -208,6 +208,7 @@ Object { "man/man1/npm-team.1", "man/man1/npm-test.1", "man/man1/npm-token.1", + "man/man1/npm-undeprecate.1", "man/man1/npm-uninstall.1", "man/man1/npm-unpublish.1", "man/man1/npm-unstar.1", diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 0ebee0d7ae896..baceb5dcd3e3c 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -155,6 +155,7 @@ Array [ "team", "test", "token", + "undeprecate", "uninstall", "unpublish", "unstar", @@ -2850,7 +2851,7 @@ Usage: npm deprecate Options: -[--registry ] [--otp ] +[--registry ] [--otp ] [--dry-run] Run "npm help deprecate" for more info @@ -2862,6 +2863,7 @@ Note: This command is unaware of workspaces. #### \`registry\` #### \`otp\` +#### \`dry-run\` ` exports[`test/lib/docs.js TAP usage diff > must match snapshot 1`] = ` @@ -4274,6 +4276,28 @@ Note: This command is unaware of workspaces. #### \`otp\` ` +exports[`test/lib/docs.js TAP usage undeprecate > must match snapshot 1`] = ` +Undeprecate a version of a package + +Usage: +npm undeprecate + +Options: +[--registry ] [--otp ] [--dry-run] + +Run "npm help undeprecate" for more info + +\`\`\`bash +npm undeprecate +\`\`\` + +Note: This command is unaware of workspaces. + +#### \`registry\` +#### \`otp\` +#### \`dry-run\` +` + exports[`test/lib/docs.js TAP usage uninstall > must match snapshot 1`] = ` Remove a package diff --git a/tap-snapshots/test/lib/npm.js.test.cjs b/tap-snapshots/test/lib/npm.js.test.cjs index 0864ffe37d297..88597c2fd15f6 100644 --- a/tap-snapshots/test/lib/npm.js.test.cjs +++ b/tap-snapshots/test/lib/npm.js.test.cjs @@ -39,7 +39,8 @@ All commands: ping, pkg, prefix, profile, prune, publish, query, rebuild, repo, restart, root, run-script, sbom, search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, unstar, update, version, view, whoami + undeprecate, uninstall, unpublish, unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -89,9 +90,10 @@ All commands: search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, - unstar, update, version, - view, whoami + undeprecate, uninstall, + unpublish, unstar, + update, version, view, + whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -141,9 +143,10 @@ All commands: search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, - unstar, update, version, - view, whoami + undeprecate, uninstall, + unpublish, unstar, + update, version, view, + whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -179,7 +182,8 @@ All commands: ping, pkg, prefix, profile, prune, publish, query, rebuild, repo, restart, root, run-script, sbom, search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, unstar, update, version, view, whoami + undeprecate, uninstall, unpublish, unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -229,9 +233,10 @@ All commands: search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, - unstar, update, version, - view, whoami + undeprecate, uninstall, + unpublish, unstar, + update, version, view, + whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -281,9 +286,10 @@ All commands: search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, - unstar, update, version, - view, whoami + undeprecate, uninstall, + unpublish, unstar, + update, version, view, + whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -331,10 +337,10 @@ All commands: run-script, sbom, search, set, shrinkwrap, star, stars, start, stop, team, - test, token, uninstall, - unpublish, unstar, - update, version, view, - whoami + test, token, undeprecate, + uninstall, unpublish, + unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -370,8 +376,8 @@ All commands: ping, pkg, prefix, profile, prune, publish, query, rebuild, repo, restart, root, run-script, sbom, search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, unstar, update, version, view, - whoami + undeprecate, uninstall, unpublish, unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -407,7 +413,8 @@ All commands: ping, pkg, prefix, profile, prune, publish, query, rebuild, repo, restart, root, run-script, sbom, search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, unstar, update, version, view, whoami + undeprecate, uninstall, unpublish, unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} @@ -443,7 +450,8 @@ All commands: ping, pkg, prefix, profile, prune, publish, query, rebuild, repo, restart, root, run-script, sbom, search, set, shrinkwrap, star, stars, start, stop, team, test, token, - uninstall, unpublish, unstar, update, version, view, whoami + undeprecate, uninstall, unpublish, unstar, update, version, + view, whoami Specify configs in the ini-formatted file: {USERCONFIG} diff --git a/test/lib/commands/undeprecate.js b/test/lib/commands/undeprecate.js new file mode 100644 index 0000000000000..775a2183a1299 --- /dev/null +++ b/test/lib/commands/undeprecate.js @@ -0,0 +1,72 @@ +const t = require('tap') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') + +const MockRegistry = require('@npmcli/mock-registry') + +const token = 'test-auth-token' +const auth = { '//registry.npmjs.org/:_authToken': token } +const versions = ['1.0.0', '1.0.1', '1.0.1-pre'] + +t.test('no args', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('undeprecate', []), + { code: 'EUSAGE' }, + 'logs usage' + ) +}) + +t.test('undeprecate', async t => { + const { npm, logs, joinedOutput } = await loadMockNpm(t, { config: { ...auth } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + const manifest = registry.manifest({ + name: 'foo', + versions, + }) + await registry.package({ manifest, query: { write: true } }) + registry.nock.put('/foo', body => { + for (const version of versions) { + if (body.versions[version].deprecated !== '') { + return false + } + } + return true + }).reply(200, {}) + + await npm.exec('undeprecate', ['foo']) + t.match(logs.notice, [ + 'undeprecating foo@1.0.0', + 'undeprecating foo@1.0.1', + 'undeprecating foo@1.0.1-pre', + ]) + t.match(joinedOutput(), '') +}) + +t.test('dry-run', async t => { + const { npm, logs, joinedOutput } = await loadMockNpm(t, { config: { + 'dry-run': true, + ...auth, + } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + const manifest = registry.manifest({ + name: 'foo', + versions, + }) + await registry.package({ manifest, query: { write: true } }) + + await npm.exec('undeprecate', ['foo']) + t.match(logs.notice, [ + 'undeprecating foo@1.0.0', + 'undeprecating foo@1.0.1', + 'undeprecating foo@1.0.1-pre', + ]) + t.match(joinedOutput(), '') +}) From 6cdfee13652ea84e11e93f63ea10ffeb55f3bd4c Mon Sep 17 00:00:00 2001 From: Gar Date: Fri, 3 Jan 2025 10:06:05 -0800 Subject: [PATCH 2/2] feat: add dry-run to deprecate/undeprecate commands --- lib/commands/deprecate.js | 24 +++++++++++++++++------- test/lib/commands/deprecate.js | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/lib/commands/deprecate.js b/lib/commands/deprecate.js index 95eaf429120fa..d8d33ad9b9d03 100644 --- a/lib/commands/deprecate.js +++ b/lib/commands/deprecate.js @@ -14,6 +14,7 @@ class Deprecate extends BaseCommand { static params = [ 'registry', 'otp', + 'dry-run', ] static ignoreImplicitWorkspace = true @@ -56,17 +57,26 @@ class Deprecate extends BaseCommand { const versions = Object.keys(packument.versions) .filter(v => semver.satisfies(v, spec, { includePrerelease: true })) + const dryRun = this.npm.config.get('dry-run') + if (versions.length) { for (const v of versions) { packument.versions[v].deprecated = msg + if (msg) { + log.notice(`deprecating ${packument.name}@${v} with message "${msg}"`) + } else { + log.notice(`undeprecating ${packument.name}@${v}`) + } + } + if (!dryRun) { + return otplease(this.npm, this.npm.flatOptions, opts => npmFetch(uri, { + ...opts, + spec: p, + method: 'PUT', + body: packument, + ignoreBody: true, + })) } - return otplease(this.npm, this.npm.flatOptions, opts => npmFetch(uri, { - ...opts, - spec: p, - method: 'PUT', - body: packument, - ignoreBody: true, - })) } else { log.warn('deprecate', 'No version found for', p.rawSpec) } diff --git a/test/lib/commands/deprecate.js b/test/lib/commands/deprecate.js index 09aaeacfe8563..eda51bfef2895 100644 --- a/test/lib/commands/deprecate.js +++ b/test/lib/commands/deprecate.js @@ -129,7 +129,7 @@ t.test('deprecates given range', async t => { }) t.test('deprecates all versions when no range is specified', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { config: { ...auth } }) + const { npm, logs, joinedOutput } = await loadMockNpm(t, { config: { ...auth } }) const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry'), @@ -151,6 +151,37 @@ t.test('deprecates all versions when no range is specified', async t => { }).reply(200, {}) await npm.exec('deprecate', ['foo', message]) + t.match(logs.notice, [ + `deprecating foo@1.0.0 with message "${message}"`, + `deprecating foo@1.0.1 with message "${message}"`, + `deprecating foo@1.0.1-pre with message "${message}"`, + ]) + t.match(joinedOutput(), '') +}) + +t.test('dry-run', async t => { + const { npm, logs, joinedOutput } = await loadMockNpm(t, { config: { + 'dry-run': true, + ...auth, + } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + const manifest = registry.manifest({ + name: 'foo', + versions, + }) + await registry.package({ manifest, query: { write: true } }) + const message = 'test deprecation message' + + await npm.exec('deprecate', ['foo', message]) + t.match(logs.notice, [ + `deprecating foo@1.0.0 with message "${message}"`, + `deprecating foo@1.0.1 with message "${message}"`, + `deprecating foo@1.0.1-pre with message "${message}"`, + ]) t.match(joinedOutput(), '') })