From 9eb120d480ab1e10c8535218b4b071425899a38b Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Thu, 17 Mar 2022 17:54:51 -0400 Subject: [PATCH] feat(version): reify on workspace version change Adds a minimalistic reify step that updates the installed tree after a version change within one of the configured workspaces when using any of the workspaces config options. It's also possible to use the `--save` config option in order to auto update semver ranges of dependencies declarations accross dependent `package.json` files. Fixes: https://github.com/npm/cli/issues/3403 Relates to: https://github.com/npm/rfcs/issues/556 Relates to: https://github.com/npm/cli/issues/3757 Relates to: https://github.com/npm/cli/issues/4193 --- docs/content/commands/npm-version.md | 11 ++ docs/content/using-npm/config.md | 11 ++ lib/commands/version.js | 35 ++++++ lib/utils/config/definitions.js | 10 ++ .../test/lib/commands/config.js.test.cjs | 2 + .../test/lib/commands/version.js.test.cjs | 94 ++++++++++++++ .../test/lib/load-all-commands.js.test.cjs | 2 +- .../lib/utils/config/definitions.js.test.cjs | 11 ++ .../lib/utils/config/describe-all.js.test.cjs | 11 ++ .../test/lib/utils/npm-usage.js.test.cjs | 2 +- test/lib/commands/version.js | 117 ++++++++++++++++-- 11 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 tap-snapshots/test/lib/commands/version.js.test.cjs diff --git a/docs/content/commands/npm-version.md b/docs/content/commands/npm-version.md index 86e2ce90e9bea..713e5ae410dfc 100644 --- a/docs/content/commands/npm-version.md +++ b/docs/content/commands/npm-version.md @@ -144,6 +144,17 @@ This value is not exported to the environment for child processes. +#### `workspaces-update` + +* Default: true +* Type: Boolean + +If set to true, the npm cli will run an update after operations that may +possibly change the workspaces installed to the `node_modules` folder. + + + + #### `include-workspace-root` * Default: false diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index df259715f08f2..76e5e35e6ae72 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -1823,6 +1823,17 @@ This value is not exported to the environment for child processes. +#### `workspaces-update` + +* Default: true +* Type: Boolean + +If set to true, the npm cli will run an update after operations that may +possibly change the workspaces installed to the `node_modules` folder. + + + + #### `yes` * Default: null diff --git a/lib/commands/version.js b/lib/commands/version.js index f49a309a74ff6..ed506f663e89f 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -3,6 +3,9 @@ const { resolve } = require('path') const { promisify } = require('util') const readFile = promisify(require('fs').readFile) +const Arborist = require('@npmcli/arborist') +const reifyFinish = require('../utils/reify-finish.js') + const BaseCommand = require('../base-command.js') class Version extends BaseCommand { @@ -17,6 +20,7 @@ class Version extends BaseCommand { 'sign-git-tag', 'workspace', 'workspaces', + 'workspaces-update', 'include-workspace-root', ] @@ -81,6 +85,7 @@ class Version extends BaseCommand { async changeWorkspaces (args, filters) { const prefix = this.npm.config.get('tag-version-prefix') await this.setWorkspaces(filters) + const updatedWorkspaces = [] for (const [name, path] of this.workspaces) { this.npm.output(name) const version = await libnpmversion(args[0], { @@ -88,8 +93,10 @@ class Version extends BaseCommand { 'git-tag-version': false, path, }) + updatedWorkspaces.push(name) this.npm.output(`${prefix}${version}`) } + return this.update(updatedWorkspaces) } async list (results = {}) { @@ -129,6 +136,34 @@ class Version extends BaseCommand { } return this.list(results) } + + async update (args) { + if (!this.npm.flatOptions.workspacesUpdate || !args.length) { + return + } + + // default behavior is to not save by default in order to avoid + // race condition problems when publishing multiple workspaces + // that have dependencies on one another, it might still be useful + // in some cases, which then need to set --save + const save = this.npm.config.isDefault('save') + ? false + : this.npm.config.get('save') + + // runs a minimalistic reify update, targetting only the workspaces + // that had version updates and skipping fund/audit/save + const opts = { + ...this.npm.flatOptions, + audit: false, + fund: false, + path: this.npm.localPrefix, + save, + } + const arb = new Arborist(opts) + + await arb.reify({ ...opts, update: args }) + await reifyFinish(this.npm, arb) + } } module.exports = Version diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index ddccb147586f4..abc989d0e61ca 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -2270,6 +2270,16 @@ define('workspaces', { }, }) +define('workspaces-update', { + default: true, + type: Boolean, + description: ` + If set to true, the npm cli will run an update after operations that may + possibly change the workspaces installed to the \`node_modules\` folder. + `, + flatten, +}) + define('yes', { default: null, type: [null, Boolean], diff --git a/tap-snapshots/test/lib/commands/config.js.test.cjs b/tap-snapshots/test/lib/commands/config.js.test.cjs index 8e97915230719..0806c68ca6571 100644 --- a/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -155,6 +155,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "which": null, "workspace": [], "workspaces": null, + "workspaces-update": true, "yes": null, "metrics-registry": "https://registry.npmjs.org/" } @@ -308,6 +309,7 @@ viewer = "{VIEWER}" which = null workspace = [] workspaces = null +workspaces-update = true yes = null ; "global" config from {GLOBALPREFIX}/npmrc diff --git a/tap-snapshots/test/lib/commands/version.js.test.cjs b/tap-snapshots/test/lib/commands/version.js.test.cjs new file mode 100644 index 0000000000000..e19f9b8eef14e --- /dev/null +++ b/tap-snapshots/test/lib/commands/version.js.test.cjs @@ -0,0 +1,94 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/commands/version.js TAP empty versions workspaces with one arg, all workspaces > must match snapshot 1`] = ` +{ + "name": "workspaces-test", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "workspaces-test", + "version": "1.0.0", + "workspaces": [ + "workspace-a", + "workspace-b" + ] + }, + "node_modules/workspace-a": { + "resolved": "workspace-a", + "link": true + }, + "node_modules/workspace-b": { + "resolved": "workspace-b", + "link": true + }, + "workspace-a": { + "version": "2.0.0" + }, + "workspace-b": { + "version": "2.0.0" + } + }, + "dependencies": { + "workspace-a": { + "version": "file:workspace-a" + }, + "workspace-b": { + "version": "file:workspace-b" + } + } +} + +` + +exports[`test/lib/commands/version.js TAP empty versions workspaces with one arg, all workspaces, saves package.json > must match snapshot 1`] = ` +{ + "name": "workspaces-test", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "workspaces-test", + "version": "1.0.0", + "workspaces": [ + "workspace-a", + "workspace-b" + ], + "dependencies": { + "workspace-a": "^2.0.0", + "workspace-b": "^2.0.0" + } + }, + "node_modules/workspace-a": { + "resolved": "workspace-a", + "link": true + }, + "node_modules/workspace-b": { + "resolved": "workspace-b", + "link": true + }, + "workspace-a": { + "version": "2.0.0" + }, + "workspace-b": { + "version": "2.0.0" + } + }, + "dependencies": { + "workspace-a": { + "version": "file:workspace-a" + }, + "workspace-b": { + "version": "file:workspace-b" + } + } +} + +` diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index 1ad8aee29f087..9fe2a5491c013 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -1092,7 +1092,7 @@ Options: [--allow-same-version] [--no-commit-hooks] [--no-git-tag-version] [--json] [--preid prerelease-id] [--sign-git-tag] [-w|--workspace [-w|--workspace ...]] -[-ws|--workspaces] [--include-workspace-root] +[-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root] alias: verison diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 373f094a59af9..4d3a6f15068ae 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -151,6 +151,7 @@ Array [ "which", "workspace", "workspaces", + "workspaces-update", "yes", ] ` @@ -1915,6 +1916,16 @@ _unless_ one or more workspaces are specified in the \`workspace\` config. This value is not exported to the environment for child processes. ` +exports[`test/lib/utils/config/definitions.js TAP > config description for workspaces-update 1`] = ` +#### \`workspaces-update\` + +* Default: true +* Type: Boolean + +If set to true, the npm cli will run an update after operations that may +possibly change the workspaces installed to the \`node_modules\` folder. +` + exports[`test/lib/utils/config/definitions.js TAP > config description for yes 1`] = ` #### \`yes\` diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index 3a7d90db01be6..94ddbe2b11bb5 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -1697,6 +1697,17 @@ This value is not exported to the environment for child processes. +#### \`workspaces-update\` + +* Default: true +* Type: Boolean + +If set to true, the npm cli will run an update after operations that may +possibly change the workspaces installed to the \`node_modules\` folder. + + + + #### \`yes\` * Default: null diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index 8f73b93f3e450..181e47da7d724 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -1125,7 +1125,7 @@ All commands: [--allow-same-version] [--no-commit-hooks] [--no-git-tag-version] [--json] [--preid prerelease-id] [--sign-git-tag] [-w|--workspace [-w|--workspace ...]] - [-ws|--workspaces] [--include-workspace-root] + [-ws|--workspaces] [--no-workspaces-update] [--include-workspace-root] alias: verison diff --git a/test/lib/commands/version.js b/test/lib/commands/version.js index 980353897c29a..154f6a6f83361 100644 --- a/test/lib/commands/version.js +++ b/test/lib/commands/version.js @@ -1,3 +1,5 @@ +const { readFileSync, statSync } = require('fs') +const { resolve } = require('path') const t = require('tap') const { fake: mockNpm } = require('../../fixtures/mock-npm') const mockGlobals = require('../../fixtures/mock-globals.js') @@ -10,8 +12,13 @@ const config = { 'tag-version-prefix': 'v', json: false, } +const flatOptions = { + workspacesUpdate: true, +} const npm = mockNpm({ config, + flatOptions, + localPrefix: '', prefix: '', version: '1.0.0', output: (...msg) => { @@ -21,14 +28,16 @@ const npm = mockNpm({ }, }) const mocks = { - libnpmversion: noop, + '../../../lib/utils/reify-finish.js': noop, } const Version = t.mock('../../../lib/commands/version.js', mocks) const version = new Version(npm) t.afterEach(() => { + flatOptions.workspacesUpdate = true config.json = false + npm.localPrefix = '' npm.prefix = '' result = [] }) @@ -120,7 +129,7 @@ t.test('empty versions', t => { ...mocks, libnpmversion: (arg, opts) => { t.equal(arg, 'major', 'should forward expected value') - t.same( + t.match( opts, { path: '', @@ -271,7 +280,6 @@ t.test('empty versions', t => { }) t.test('with one arg, all workspaces', async t => { - const libNpmVersionArgs = [] const testDir = t.testdir({ 'package.json': JSON.stringify( { @@ -296,12 +304,54 @@ t.test('empty versions', t => { }, }) const Version = t.mock('../../../lib/commands/version.js', { - ...mocks, - libnpmversion: (arg, opts) => { - libNpmVersionArgs.push([arg, opts]) - return '2.0.0' + '../../../lib/utils/reify-finish.js': noop, + }) + npm.localPrefix = testDir + npm.prefix = testDir + const version = new Version(npm) + + await version.execWorkspaces(['major'], []) + t.same( + result, + ['workspace-a', 'v2.0.0', 'workspace-b', 'v2.0.0'], + 'outputs the new version for only the workspaces prefixed by the tagVersionPrefix' + ) + + t.matchSnapshot(readFileSync(resolve(testDir, 'package-lock.json'), 'utf8')) + }) + + t.test('with one arg, all workspaces, saves package.json', async t => { + const testDir = t.testdir({ + 'package.json': JSON.stringify( + { + name: 'workspaces-test', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b'], + dependencies: { + 'workspace-a': '^1.0.0', + 'workspace-b': '^1.0.0', + }, + }, + null, + 2 + ), + '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', + }), + }, + }) + const Version = t.mock('../../../lib/commands/version.js', { + '../../../lib/utils/reify-finish.js': noop, }) + config.save = true npm.localPrefix = testDir npm.prefix = testDir const version = new Version(npm) @@ -312,6 +362,8 @@ t.test('empty versions', t => { ['workspace-a', 'v2.0.0', 'workspace-b', 'v2.0.0'], 'outputs the new version for only the workspaces prefixed by the tagVersionPrefix' ) + + t.matchSnapshot(readFileSync(resolve(testDir, 'package-lock.json'), 'utf8')) }) t.test('too many args', async t => { @@ -321,6 +373,57 @@ t.test('empty versions', t => { 'should throw usage instructions error' ) }) + + t.test('no workspaces-update', async t => { + flatOptions.workspacesUpdate = false + + const libNpmVersionArgs = [] + const testDir = t.testdir({ + 'package.json': JSON.stringify( + { + name: 'workspaces-test', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b'], + }, + null, + 2 + ), + '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', + }), + }, + }) + const Version = t.mock('../../../lib/commands/version.js', { + ...mocks, + libnpmversion: (arg, opts) => { + libNpmVersionArgs.push([arg, opts]) + return '2.0.0' + }, + }) + npm.localPrefix = testDir + npm.prefix = testDir + const version = new Version(npm) + + await version.execWorkspaces(['major'], []) + t.same( + result, + ['workspace-a', 'v2.0.0', 'workspace-b', 'v2.0.0'], + 'outputs the new version for only the workspaces prefixed by the tagVersionPrefix' + ) + + t.throws( + () => statSync(resolve(testDir, 'package-lock.json')), + 'should not have a lockfile since have not reified' + ) + }) }) t.end()