diff --git a/common/changes/@microsoft/rush/iclanton-fix-make-consistent_2025-10-07-18-30.json b/common/changes/@microsoft/rush/iclanton-fix-make-consistent_2025-10-07-18-30.json new file mode 100644 index 00000000000..a6f23c8db6a --- /dev/null +++ b/common/changes/@microsoft/rush/iclanton-fix-make-consistent_2025-10-07-18-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix an issue where `rush add --make-consistent ...` may drop the `implicitlyPreferredVersions` and `ensureConsistentVersions` properties from `common/config/rush/common-versions.json`.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 5e77d5f2e43..c594b13092c 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -103,7 +103,7 @@ "policyName": "rush", "definitionName": "lockStepVersion", "version": "5.160.1", - "nextBump": "patch", + "nextBump": "minor", "mainProject": "@microsoft/rush" } ] diff --git a/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts b/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts index 9333d06d04f..0996bbc5a0f 100644 --- a/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts +++ b/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts @@ -13,6 +13,7 @@ import { Sort } from '@rushstack/node-core-library'; +import type { OptionalToUndefined } from '../utilities/Utilities'; import { PackageNameParsers } from './PackageNameParsers'; import { JsonSchemaUrls } from '../logic/JsonSchemaUrls'; import type { RushConfiguration } from './RushConfiguration'; @@ -67,6 +68,7 @@ export class CommonVersionsConfiguration { private _preferredVersions: ProtectableMap; private _allowedAlternativeVersions: ProtectableMap; private _modified: boolean = false; + private _commonVersionsJsonHasEnsureConsistentVersionsProperty: boolean; /** * Get the absolute file path of the common-versions.json file. @@ -163,6 +165,8 @@ export class CommonVersionsConfiguration { this.ensureConsistentVersions = commonVersionsEnsureConsistentVersions ?? rushJsonEnsureConsistentVersions ?? false; + this._commonVersionsJsonHasEnsureConsistentVersionsProperty = + commonVersionsEnsureConsistentVersions !== undefined; if (commonVersionsJson) { try { @@ -242,7 +246,10 @@ export class CommonVersionsConfiguration { */ public save(): boolean { if (this._modified) { - JsonFile.save(this._serialize(), this.filePath, { updateExistingFile: true }); + JsonFile.save(this._serialize(), this.filePath, { + updateExistingFile: true, + ignoreUndefinedValues: true + }); this._modified = false; return true; } @@ -284,20 +291,27 @@ export class CommonVersionsConfiguration { } private _serialize(): ICommonVersionsJson { - const result: ICommonVersionsJson = { - $schema: JsonSchemaUrls.commonVersions - }; - + let preferredVersions: ICommonVersionsJsonVersionMap | undefined; if (this._preferredVersions.size) { - result.preferredVersions = CommonVersionsConfiguration._serializeTable(this.preferredVersions); + preferredVersions = CommonVersionsConfiguration._serializeTable(this.preferredVersions); } + let allowedAlternativeVersions: ICommonVersionsJsonVersionsMap | undefined; if (this._allowedAlternativeVersions.size) { - result.allowedAlternativeVersions = CommonVersionsConfiguration._serializeTable( + allowedAlternativeVersions = CommonVersionsConfiguration._serializeTable( this.allowedAlternativeVersions ) as ICommonVersionsJsonVersionsMap; } + const result: OptionalToUndefined = { + $schema: JsonSchemaUrls.commonVersions, + preferredVersions, + implicitlyPreferredVersions: this.implicitlyPreferredVersions, + allowedAlternativeVersions, + ensureConsistentVersions: this._commonVersionsJsonHasEnsureConsistentVersionsProperty + ? this.ensureConsistentVersions + : undefined + }; return result; } } diff --git a/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap b/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap index 3235af39fcf..c47899ac293 100644 --- a/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap +++ b/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap @@ -22,6 +22,22 @@ Object { "required": true, "shortName": "-p", }, + Object { + "description": "If specified, the dependency will be added to all projects.", + "environmentVariable": undefined, + "kind": "Flag", + "longName": "--all", + "required": false, + "shortName": undefined, + }, + Object { + "description": "Run command using a variant installation configuration", + "environmentVariable": "RUSH_VARIANT", + "kind": "String", + "longName": "--variant", + "required": false, + "shortName": undefined, + }, Object { "description": "If specified, the SemVer specifier added to the package.json will be an exact version (e.g. without tilde or caret).", "environmentVariable": undefined, @@ -62,22 +78,6 @@ Object { "required": false, "shortName": "-m", }, - Object { - "description": "If specified, the dependency will be added to all projects.", - "environmentVariable": undefined, - "kind": "Flag", - "longName": "--all", - "required": false, - "shortName": undefined, - }, - Object { - "description": "Run command using a variant installation configuration", - "environmentVariable": "RUSH_VARIANT", - "kind": "String", - "longName": "--variant", - "required": false, - "shortName": undefined, - }, ], }, Object { diff --git a/libraries/rush-lib/src/cli/actions/AddAction.ts b/libraries/rush-lib/src/cli/actions/AddAction.ts index 9e9f23d53c1..db67f19ea0c 100644 --- a/libraries/rush-lib/src/cli/actions/AddAction.ts +++ b/libraries/rush-lib/src/cli/actions/AddAction.ts @@ -3,13 +3,9 @@ import * as semver from 'semver'; -import type { - CommandLineFlagParameter, - CommandLineStringListParameter, - CommandLineStringParameter -} from '@rushstack/ts-command-line'; +import type { CommandLineFlagParameter } from '@rushstack/ts-command-line'; -import { BaseAddAndRemoveAction } from './BaseAddAndRemoveAction'; +import { BaseAddAndRemoveAction, PACKAGE_PARAMETER_NAME } from './BaseAddAndRemoveAction'; import type { RushCommandLineParser } from '../RushCommandLineParser'; import { DependencySpecifier } from '../../logic/DependencySpecifier'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; @@ -18,17 +14,19 @@ import { type IPackageJsonUpdaterRushAddOptions, SemVerStyle } from '../../logic/PackageJsonUpdaterTypes'; -import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; +import { getVariantAsync } from '../../api/Variants'; + +const ADD_ACTION_NAME: 'add' = 'add'; +export const MAKE_CONSISTENT_FLAG_NAME: '--make-consistent' = '--make-consistent'; +const EXACT_FLAG_NAME: '--exact' = '--exact'; +const CARET_FLAG_NAME: '--caret' = '--caret'; export class AddAction extends BaseAddAndRemoveAction { - protected readonly _allFlag: CommandLineFlagParameter; - protected readonly _packageNameList: CommandLineStringListParameter; private readonly _exactFlag: CommandLineFlagParameter; private readonly _caretFlag: CommandLineFlagParameter; private readonly _devDependencyFlag: CommandLineFlagParameter; private readonly _peerDependencyFlag: CommandLineFlagParameter; private readonly _makeConsistentFlag: CommandLineFlagParameter; - private readonly _variantParameter: CommandLineStringParameter; public constructor(parser: RushCommandLineParser) { const documentation: string = [ @@ -36,37 +34,32 @@ export class AddAction extends BaseAddAndRemoveAction { ' and then runs "rush update". If no version is specified, a version will be automatically detected (typically' + ' either the latest version or a version that won\'t break the "ensureConsistentVersions" policy). If a version' + ' range (or a workspace range) is specified, the latest version in the range will be used. The version will be' + - ' automatically prepended with a tilde, unless the "--exact" or "--caret" flags are used. The "--make-consistent"' + - ' flag can be used to update all packages with the dependency.' + ` automatically prepended with a tilde, unless the "${EXACT_FLAG_NAME}" or "${CARET_FLAG_NAME}" flags are used.` + + ` The "${MAKE_CONSISTENT_FLAG_NAME}" flag can be used to update all packages with the dependency.` ].join('\n'); super({ - actionName: 'add', + actionName: ADD_ACTION_NAME, summary: 'Adds one or more dependencies to the package.json and runs rush update.', documentation, safeForSimultaneousRushProcesses: false, - parser - }); - - this._packageNameList = this.defineStringListParameter({ - parameterLongName: '--package', - parameterShortName: '-p', - required: true, - argumentName: 'PACKAGE', - description: + parser, + allFlagDescription: 'If specified, the dependency will be added to all projects.', + packageNameListParameterDescription: 'The name of the package which should be added as a dependency.' + ' A SemVer version specifier can be appended after an "@" sign. WARNING: Symbol characters' + " are usually interpreted by your shell, so it's recommended to use quotes." + - ' For example, write "rush add --package "example@^1.2.3"" instead of "rush add --package example@^1.2.3".' + - ' To add multiple packages, write "rush add --package foo --package bar".' + ` For example, write "rush add ${PACKAGE_PARAMETER_NAME} "example@^1.2.3"" instead of "rush add ${PACKAGE_PARAMETER_NAME} example@^1.2.3".` + + ` To add multiple packages, write "rush add ${PACKAGE_PARAMETER_NAME} foo ${PACKAGE_PARAMETER_NAME} bar".` }); + this._exactFlag = this.defineFlagParameter({ - parameterLongName: '--exact', + parameterLongName: EXACT_FLAG_NAME, description: 'If specified, the SemVer specifier added to the' + ' package.json will be an exact version (e.g. without tilde or caret).' }); this._caretFlag = this.defineFlagParameter({ - parameterLongName: '--caret', + parameterLongName: CARET_FLAG_NAME, description: 'If specified, the SemVer specifier added to the' + ' package.json will be a prepended with a "caret" specifier ("^").' @@ -82,17 +75,12 @@ export class AddAction extends BaseAddAndRemoveAction { 'If specified, the package will be added to the "peerDependencies" section of the package.json' }); this._makeConsistentFlag = this.defineFlagParameter({ - parameterLongName: '--make-consistent', + parameterLongName: MAKE_CONSISTENT_FLAG_NAME, parameterShortName: '-m', description: 'If specified, other packages with this dependency will have their package.json' + ' files updated to use the same version of the dependency.' }); - this._allFlag = this.defineFlagParameter({ - parameterLongName: '--all', - description: 'If specified, the dependency will be added to all projects.' - }); - this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } public async getUpdateOptionsAsync(): Promise { @@ -142,7 +130,7 @@ export class AddAction extends BaseAddAndRemoveAction { if (this._exactFlag.value || this._caretFlag.value) { throw new Error( `The "${this._caretFlag.longName}" and "${this._exactFlag.longName}" flags may not be specified if a ` + - `version is provided in the ${this._packageNameList.longName} specifier. In this case "${version}" was provided.` + `version is provided in the ${this._packageNameListParameter.longName} specifier. In this case "${version}" was provided.` ); } @@ -165,7 +153,7 @@ export class AddAction extends BaseAddAndRemoveAction { ); return { - projects: projects, + projects, packagesToUpdate: packagesToAdd, devDependency: this._devDependencyFlag.value, peerDependency: this._peerDependencyFlag.value, diff --git a/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts b/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts index 75ef6b4339f..9e0c4364548 100644 --- a/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts +++ b/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts @@ -1,7 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { CommandLineFlagParameter, CommandLineStringListParameter } from '@rushstack/ts-command-line'; +import type { + CommandLineFlagParameter, + CommandLineStringListParameter, + CommandLineStringParameter +} from '@rushstack/ts-command-line'; import { BaseRushAction, type IBaseRushActionOptions } from './BaseRushAction'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; @@ -11,6 +15,9 @@ import type { IPackageJsonUpdaterRushBaseUpdateOptions } from '../../logic/PackageJsonUpdaterTypes'; import { RushConstants } from '../../logic/RushConstants'; +import { VARIANT_PARAMETER } from '../../api/Variants'; + +export const PACKAGE_PARAMETER_NAME: '--package' = '--package'; export interface IBasePackageJsonUpdaterRushOptions { /** @@ -31,27 +38,50 @@ export interface IBasePackageJsonUpdaterRushOptions { debugInstall: boolean; } +export interface IBaseAddAndRemoveActionOptions extends IBaseRushActionOptions { + allFlagDescription: string; + packageNameListParameterDescription: string; +} + /** * This is the common base class for AddAction and RemoveAction. */ export abstract class BaseAddAndRemoveAction extends BaseRushAction { - protected abstract readonly _allFlag: CommandLineFlagParameter; - protected readonly _skipUpdateFlag!: CommandLineFlagParameter; - protected abstract readonly _packageNameList: CommandLineStringListParameter; + protected readonly _skipUpdateFlag: CommandLineFlagParameter; + protected readonly _packageNameListParameter: CommandLineStringListParameter; + protected readonly _allFlag: CommandLineFlagParameter; + protected readonly _variantParameter: CommandLineStringParameter; protected get specifiedPackageNameList(): readonly string[] { - return this._packageNameList.values!; + return this._packageNameListParameter.values; } - public constructor(options: IBaseRushActionOptions) { + public constructor(options: IBaseAddAndRemoveActionOptions) { super(options); + const { packageNameListParameterDescription, allFlagDescription } = options; + this._skipUpdateFlag = this.defineFlagParameter({ parameterLongName: '--skip-update', parameterShortName: '-s', description: 'If specified, the "rush update" command will not be run after updating the package.json files.' }); + + this._packageNameListParameter = this.defineStringListParameter({ + parameterLongName: PACKAGE_PARAMETER_NAME, + parameterShortName: '-p', + required: true, + argumentName: 'PACKAGE', + description: packageNameListParameterDescription + }); + + this._allFlag = this.defineFlagParameter({ + parameterLongName: '--all', + description: allFlagDescription + }); + + this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } protected abstract getUpdateOptionsAsync(): Promise; diff --git a/libraries/rush-lib/src/cli/actions/RemoveAction.ts b/libraries/rush-lib/src/cli/actions/RemoveAction.ts index bb94c937e62..72c17f44d55 100644 --- a/libraries/rush-lib/src/cli/actions/RemoveAction.ts +++ b/libraries/rush-lib/src/cli/actions/RemoveAction.ts @@ -1,53 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { - CommandLineFlagParameter, - CommandLineStringListParameter, - CommandLineStringParameter -} from '@rushstack/ts-command-line'; - -import { BaseAddAndRemoveAction } from './BaseAddAndRemoveAction'; +import { BaseAddAndRemoveAction, PACKAGE_PARAMETER_NAME } from './BaseAddAndRemoveAction'; import type { RushCommandLineParser } from '../RushCommandLineParser'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import type { IPackageForRushRemove, IPackageJsonUpdaterRushRemoveOptions } from '../../logic/PackageJsonUpdaterTypes'; -import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; +import { getVariantAsync } from '../../api/Variants'; -export class RemoveAction extends BaseAddAndRemoveAction { - protected readonly _allFlag: CommandLineFlagParameter; - protected readonly _packageNameList: CommandLineStringListParameter; - private readonly _variantParameter: CommandLineStringParameter; +const REMOVE_ACTION_NAME: 'remove' = 'remove'; +export class RemoveAction extends BaseAddAndRemoveAction { public constructor(parser: RushCommandLineParser) { const documentation: string = [ 'Removes specified package(s) from the dependencies of the current project (as determined by the current working directory)' + ' and then runs "rush update".' ].join('\n'); super({ - actionName: 'remove', + actionName: REMOVE_ACTION_NAME, summary: 'Removes one or more dependencies from the package.json and runs rush update.', documentation, safeForSimultaneousRushProcesses: false, - parser - }); + parser, - this._packageNameList = this.defineStringListParameter({ - parameterLongName: '--package', - parameterShortName: '-p', - required: true, - argumentName: 'PACKAGE', - description: + packageNameListParameterDescription: 'The name of the package which should be removed.' + - ' To remove multiple packages, run "rush remove --package foo --package bar".' - }); - this._allFlag = this.defineFlagParameter({ - parameterLongName: '--all', - description: 'If specified, the dependency will be removed from all projects that declare it.' + ` To remove multiple packages, run "rush ${REMOVE_ACTION_NAME} ${PACKAGE_PARAMETER_NAME} foo ${PACKAGE_PARAMETER_NAME} bar".`, + allFlagDescription: 'If specified, the dependency will be removed from all projects that declare it.' }); - this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } public async getUpdateOptionsAsync(): Promise { @@ -56,27 +38,22 @@ export class RemoveAction extends BaseAddAndRemoveAction { const packagesToRemove: IPackageForRushRemove[] = []; for (const specifiedPackageName of this.specifiedPackageNameList) { - /** - * Name - */ - const packageName: string = specifiedPackageName; - - if (!this.rushConfiguration.packageNameParser.isValidName(packageName)) { - throw new Error(`The package name "${packageName}" is not valid.`); + if (!this.rushConfiguration.packageNameParser.isValidName(specifiedPackageName)) { + throw new Error(`The package name "${specifiedPackageName}" is not valid.`); } for (const project of projects) { if ( - !project.packageJsonEditor.tryGetDependency(packageName) && - !project.packageJsonEditor.tryGetDevDependency(packageName) + !project.packageJsonEditor.tryGetDependency(specifiedPackageName) && + !project.packageJsonEditor.tryGetDevDependency(specifiedPackageName) ) { this.terminal.writeLine( - `The project "${project.packageName}" does not have "${packageName}" in package.json.` + `The project "${project.packageName}" does not have "${specifiedPackageName}" in package.json.` ); } } - packagesToRemove.push({ packageName }); + packagesToRemove.push({ packageName: specifiedPackageName }); } const variant: string | undefined = await getVariantAsync( @@ -86,7 +63,7 @@ export class RemoveAction extends BaseAddAndRemoveAction { ); return { - projects: projects, + projects, packagesToUpdate: packagesToRemove, skipUpdate: this._skipUpdateFlag.value, debugInstall: this.parser.isDebug, diff --git a/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index c82ab63b20a..df9d2c0a674 100644 --- a/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -87,8 +87,8 @@ Optional arguments: `; exports[`CommandLineHelp prints the help for each action: add 1`] = ` -"usage: rush add [-h] [-s] -p PACKAGE [--exact] [--caret] [--dev] [--peer] [-m] - [--all] [--variant VARIANT] +"usage: rush add [-h] [-s] -p PACKAGE [--all] [--variant VARIANT] [--exact] + [--caret] [--dev] [--peer] [-m] Adds specified package(s) to the dependencies of the current project (as @@ -115,6 +115,11 @@ Optional arguments: \\"rush add --package example@^1.2.3\\". To add multiple packages, write \\"rush add --package foo --package bar\\". + --all If specified, the dependency will be added to all + projects. + --variant VARIANT Run command using a variant installation + configuration. This parameter may alternatively be + specified via the RUSH_VARIANT environment variable. --exact If specified, the SemVer specifier added to the package.json will be an exact version (e.g. without tilde or caret). @@ -129,11 +134,6 @@ Optional arguments: If specified, other packages with this dependency will have their package.json files updated to use the same version of the dependency. - --all If specified, the dependency will be added to all - projects. - --variant VARIANT Run command using a variant installation - configuration. This parameter may alternatively be - specified via the RUSH_VARIANT environment variable. " `; diff --git a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts index 4a6091984d6..270dcfc87bc 100644 --- a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts @@ -31,6 +31,7 @@ import { SemVerStyle } from './PackageJsonUpdaterTypes'; import type { Subspace } from '../api/Subspace'; +import { MAKE_CONSISTENT_FLAG_NAME } from '../cli/actions/AddAction'; /** * Options for adding a dependency to a particular project. @@ -409,7 +410,7 @@ export class PackageJsonUpdater { const existingVersionList: string = Array.from(existingSpecifiedVersions).join(', '); throw new Error( `Adding '${packageName}@${version}' ` + - `causes mismatched dependencies. Use the "--make-consistent" flag to update other packages to use ` + + `causes mismatched dependencies. Use the "${MAKE_CONSISTENT_FLAG_NAME}" flag to update other packages to use ` + `this version, or try specify one of the existing versions (${existingVersionList}).` ); } diff --git a/libraries/rush-lib/src/utilities/Utilities.ts b/libraries/rush-lib/src/utilities/Utilities.ts index dac1ec89015..e403ccacbd2 100644 --- a/libraries/rush-lib/src/utilities/Utilities.ts +++ b/libraries/rush-lib/src/utilities/Utilities.ts @@ -153,6 +153,14 @@ interface ICreateEnvironmentForRushCommandOptions { pathOptions?: ICreateEnvironmentForRushCommandPathOptions; } +type OptionalKeys = { + [K in keyof T]-?: {} extends Pick ? K : never; +}[keyof T]; + +export type OptionalToUndefined = Omit> & { + [K in OptionalKeys]-?: Exclude | undefined; +}; + export class Utilities { public static syncNpmrc: typeof syncNpmrc = syncNpmrc;