diff --git a/common/changes/@microsoft/rush/rework-PackageJsonEditor_2026-03-24-00-00.json b/common/changes/@microsoft/rush/rework-PackageJsonEditor_2026-03-24-00-00.json new file mode 100644 index 00000000000..2dbff73d652 --- /dev/null +++ b/common/changes/@microsoft/rush/rework-PackageJsonEditor_2026-03-24-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Add async variants of disk-touching APIs in `PackageJsonEditor` (`loadAsync`, `saveIfModifiedAsync`), `CommonVersionsConfiguration` (`loadFromFileAsync`, `saveAsync`), and `VersionPolicy` (`setDependenciesBeforePublishAsync`, `setDependenciesBeforeCommitAsync`); deprecate corresponding sync methods.", + "type": "minor", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "iclanton@users.noreply.github.com" +} diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 79f6dab782e..dbbb440e40b 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -137,9 +137,13 @@ export class CommonVersionsConfiguration { getAllPreferredVersions(): Map; getPreferredVersionsHash(): string; readonly implicitlyPreferredVersions: boolean | undefined; + // @deprecated (undocumented) static loadFromFile(jsonFilePath: string, rushConfiguration?: RushConfiguration): CommonVersionsConfiguration; + static loadFromFileAsync(jsonFilePath: string, rushConfiguration?: RushConfiguration): Promise; readonly preferredVersions: Map; + // @deprecated (undocumented) save(): boolean; + saveAsync(): Promise; } export { CredentialCache } @@ -1097,15 +1101,19 @@ export class PackageJsonEditor { readonly filePath: string; // (undocumented) static fromObject(object: IPackageJson, filename: string): PackageJsonEditor; - // (undocumented) + // @deprecated (undocumented) static load(filePath: string): PackageJsonEditor; // (undocumented) + static loadAsync(filePath: string): Promise; + // (undocumented) get name(): string; // (undocumented) removeDependency(packageName: string, dependencyType: DependencyType): void; get resolutionsList(): ReadonlyArray; - // (undocumented) + // @deprecated (undocumented) saveIfModified(): boolean; + // (undocumented) + saveIfModifiedAsync(): Promise; saveToObject(): IPackageJson; // (undocumented) tryGetDependency(packageName: string): PackageJsonDependency | undefined; @@ -1648,8 +1656,12 @@ export abstract class VersionPolicy { // @internal static load(versionPolicyJson: IVersionPolicyJson): VersionPolicy | undefined; get policyName(): string; + // @deprecated (undocumented) setDependenciesBeforeCommit(packageName: string, configuration: RushConfiguration): void; + setDependenciesBeforeCommitAsync(packageName: string, configuration: RushConfiguration): Promise; + // @deprecated (undocumented) setDependenciesBeforePublish(packageName: string, configuration: RushConfiguration): void; + setDependenciesBeforePublishAsync(packageName: string, configuration: RushConfiguration): Promise; abstract validate(versionString: string, packageName: string): void; } diff --git a/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts b/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts index 0996bbc5a0f..b93cd7b59cf 100644 --- a/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts +++ b/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts @@ -186,17 +186,42 @@ export class CommonVersionsConfiguration { } /** - * Loads the common-versions.json data from the specified file path. - * If the file has not been created yet, then an empty object is returned. + * @deprecated Use {@link CommonVersionsConfiguration.loadFromFileAsync} method instead. */ public static loadFromFile( jsonFilePath: string, rushConfiguration?: RushConfiguration ): CommonVersionsConfiguration { let commonVersionsJson: ICommonVersionsJson | undefined = undefined; - - if (FileSystem.exists(jsonFilePath)) { + try { commonVersionsJson = JsonFile.loadAndValidate(jsonFilePath, CommonVersionsConfiguration._jsonSchema); + } catch (error) { + if (!FileSystem.isNotExistError(error)) { + throw error; + } + } + + return new CommonVersionsConfiguration(commonVersionsJson, jsonFilePath, rushConfiguration); + } + + /** + * Loads the common-versions.json data from the specified file path. + * If the file has not been created yet, then an empty object is returned. + */ + public static async loadFromFileAsync( + jsonFilePath: string, + rushConfiguration?: RushConfiguration + ): Promise { + let commonVersionsJson: ICommonVersionsJson | undefined = undefined; + try { + commonVersionsJson = await JsonFile.loadAndValidateAsync( + jsonFilePath, + CommonVersionsConfiguration._jsonSchema + ); + } catch (error) { + if (!FileSystem.isNotExistError(error)) { + throw error; + } } return new CommonVersionsConfiguration(commonVersionsJson, jsonFilePath, rushConfiguration); @@ -242,7 +267,7 @@ export class CommonVersionsConfiguration { } /** - * Writes the "common-versions.json" file to disk, using the filename that was passed to loadFromFile(). + * @deprecated Use {@link CommonVersionsConfiguration.saveAsync} method instead. */ public save(): boolean { if (this._modified) { @@ -257,6 +282,22 @@ export class CommonVersionsConfiguration { return false; } + /** + * Writes the "common-versions.json" file to disk, using the filename that was passed to loadFromFile(). + */ + public async saveAsync(): Promise { + if (this._modified) { + await JsonFile.saveAsync(this._serialize(), this.filePath, { + updateExistingFile: true, + ignoreUndefinedValues: true + }); + this._modified = false; + return true; + } + + return false; + } + /** * Returns preferredVersions. */ diff --git a/libraries/rush-lib/src/api/PackageJsonEditor.ts b/libraries/rush-lib/src/api/PackageJsonEditor.ts index 27106a38ad1..c9baeb73e0f 100644 --- a/libraries/rush-lib/src/api/PackageJsonEditor.ts +++ b/libraries/rush-lib/src/api/PackageJsonEditor.ts @@ -97,97 +97,89 @@ export class PackageJsonEditor { this._sourceData = data; this._modified = false; - this._dependencies = new Map(); - this._devDependencies = new Map(); - this._resolutions = new Map(); - this._dependenciesMeta = new Map(); - - const dependencies: { [key: string]: string } = data.dependencies || {}; - const optionalDependencies: { [key: string]: string } = data.optionalDependencies || {}; - const peerDependencies: { [key: string]: string } = data.peerDependencies || {}; - - const devDependencies: { [key: string]: string } = data.devDependencies || {}; - const resolutions: { [key: string]: string } = data.resolutions || {}; - - const dependenciesMeta: { [key: string]: { [key: string]: boolean } } = data.dependenciesMeta || {}; + const { + dependencies = {}, + optionalDependencies = {}, + peerDependencies = {}, + devDependencies = {}, + resolutions = {}, + dependenciesMeta = {} + } = data; const _onChange: () => void = this._onChange.bind(this); + const optionalDependenciesSet: Set = new Set(Object.keys(optionalDependencies)); + const peerDependenciesSet: Set = new Set(Object.keys(peerDependencies)); try { - Object.keys(dependencies || {}).forEach((packageName: string) => { - if (Object.prototype.hasOwnProperty.call(optionalDependencies, packageName)) { - throw new Error( - `The package "${packageName}" cannot be listed in both ` + - `"dependencies" and "optionalDependencies"` - ); - } - if (Object.prototype.hasOwnProperty.call(peerDependencies, packageName)) { - throw new Error( - `The package "${packageName}" cannot be listed in both "dependencies" and "peerDependencies"` - ); - } + const dependenciesMapEntries: [string, PackageJsonDependency][] = Object.entries(dependencies).map( + ([packageName, version]: [string, string]) => { + if (optionalDependenciesSet.has(packageName)) { + throw new Error( + `The package "${packageName}" cannot be listed in both ` + + `"dependencies" and "optionalDependencies"` + ); + } + if (peerDependenciesSet.has(packageName)) { + throw new Error( + `The package "${packageName}" cannot be listed in both "dependencies" and "peerDependencies"` + ); + } - this._dependencies.set( - packageName, - new PackageJsonDependency(packageName, dependencies[packageName], DependencyType.Regular, _onChange) - ); - }); + return [ + packageName, + new PackageJsonDependency(packageName, version, DependencyType.Regular, _onChange) + ]; + } + ); - Object.keys(optionalDependencies || {}).forEach((packageName: string) => { - if (Object.prototype.hasOwnProperty.call(peerDependencies, packageName)) { + const optionalDependenciesMapEntries: [string, PackageJsonDependency][] = Object.entries( + optionalDependencies + ).map(([packageName, version]) => { + if (peerDependenciesSet.has(packageName)) { throw new Error( `The package "${packageName}" cannot be listed in both ` + `"optionalDependencies" and "peerDependencies"` ); } - this._dependencies.set( - packageName, - new PackageJsonDependency( - packageName, - optionalDependencies[packageName], - DependencyType.Optional, - _onChange - ) - ); - }); - - Object.keys(peerDependencies || {}).forEach((packageName: string) => { - this._dependencies.set( + return [ packageName, - new PackageJsonDependency( - packageName, - peerDependencies[packageName], - DependencyType.Peer, - _onChange - ) - ); + new PackageJsonDependency(packageName, version, DependencyType.Optional, _onChange) + ]; }); - Object.keys(devDependencies || {}).forEach((packageName: string) => { - this._devDependencies.set( + const peerDependenciesMapEntries: [string, PackageJsonDependency][] = Object.entries( + peerDependencies + ).map(([packageName, version]) => [ + packageName, + new PackageJsonDependency(packageName, version, DependencyType.Peer, _onChange) + ]); + + this._dependencies = new Map([ + ...dependenciesMapEntries, + ...optionalDependenciesMapEntries, + ...peerDependenciesMapEntries + ]); + + this._devDependencies = new Map( + Object.entries(devDependencies).map(([packageName, version]) => [ packageName, - new PackageJsonDependency(packageName, devDependencies[packageName], DependencyType.Dev, _onChange) - ); - }); + new PackageJsonDependency(packageName, version, DependencyType.Dev, _onChange) + ]) + ); - Object.keys(resolutions || {}).forEach((packageName: string) => { - this._resolutions.set( + this._resolutions = new Map( + Object.entries(resolutions).map(([packageName, version]) => [ packageName, - new PackageJsonDependency( - packageName, - resolutions[packageName], - DependencyType.YarnResolutions, - _onChange - ) - ); - }); + new PackageJsonDependency(packageName, version, DependencyType.YarnResolutions, _onChange) + ]) + ); - Object.keys(dependenciesMeta || {}).forEach((packageName: string) => { - this._dependenciesMeta.set( + this._dependenciesMeta = new Map( + Object.entries(dependenciesMeta).map(([packageName, { injected = false }]) => [ packageName, - new PackageJsonDependencyMeta(packageName, dependenciesMeta[packageName].injected, _onChange) - ); - }); + new PackageJsonDependencyMeta(packageName, injected, _onChange) + ]) + ); // (Do not sort this._resolutions because order may be significant; the RFC is unclear about that.) Sort.sortMapKeys(this._dependencies); @@ -197,8 +189,17 @@ export class PackageJsonEditor { } } + /** + * @deprecated Use {@link PackageJsonEditor.loadAsync} method instead. + */ public static load(filePath: string): PackageJsonEditor { - return new PackageJsonEditor(filePath, JsonFile.load(filePath)); + const packageJson: IPackageJson = JsonFile.load(filePath); + return new PackageJsonEditor(filePath, packageJson); + } + + public static async loadAsync(filePath: string): Promise { + const packageJson: IPackageJson = await JsonFile.loadAsync(filePath); + return new PackageJsonEditor(filePath, packageJson); } public static fromObject(object: IPackageJson, filename: string): PackageJsonEditor { @@ -270,17 +271,24 @@ export class PackageJsonEditor { switch (dependencyType) { case DependencyType.Regular: case DependencyType.Optional: - case DependencyType.Peer: + case DependencyType.Peer: { this._dependencies.set(packageName, dependency); break; - case DependencyType.Dev: + } + + case DependencyType.Dev: { this._devDependencies.set(packageName, dependency); break; - case DependencyType.YarnResolutions: + } + + case DependencyType.YarnResolutions: { this._resolutions.set(packageName, dependency); break; - default: + } + + default: { throw new InternalError('Unsupported DependencyType'); + } } this._modified = true; @@ -290,22 +298,32 @@ export class PackageJsonEditor { switch (dependencyType) { case DependencyType.Regular: case DependencyType.Optional: - case DependencyType.Peer: + case DependencyType.Peer: { this._dependencies.delete(packageName); break; - case DependencyType.Dev: + } + + case DependencyType.Dev: { this._devDependencies.delete(packageName); break; - case DependencyType.YarnResolutions: + } + + case DependencyType.YarnResolutions: { this._resolutions.delete(packageName); break; - default: + } + + default: { throw new InternalError('Unsupported DependencyType'); + } } this._modified = true; } + /** + * @deprecated Use {@link PackageJsonEditor.saveIfModifiedAsync} method instead. + */ public saveIfModified(): boolean { if (this._modified) { this._modified = false; @@ -316,6 +334,21 @@ export class PackageJsonEditor { }); return true; } + + return false; + } + + public async saveIfModifiedAsync(): Promise { + if (this._modified) { + this._modified = false; + this._sourceData = this._normalize(this._sourceData); + await JsonFile.saveAsync(this._sourceData, this.filePath, { + updateExistingFile: true, + jsonSyntax: JsonSyntax.Strict + }); + return true; + } + return false; } @@ -353,53 +386,64 @@ export class PackageJsonEditor { const keys: string[] = [...this._dependencies.keys()].sort(); for (const packageName of keys) { - const dependency: PackageJsonDependency = this._dependencies.get(packageName)!; + const { dependencyType, name, version }: PackageJsonDependency = this._dependencies.get(packageName)!; - switch (dependency.dependencyType) { - case DependencyType.Regular: + switch (dependencyType) { + case DependencyType.Regular: { if (!normalizedData.dependencies) { normalizedData.dependencies = {}; } - normalizedData.dependencies[dependency.name] = dependency.version; + + normalizedData.dependencies[name] = version; break; - case DependencyType.Optional: + } + + case DependencyType.Optional: { if (!normalizedData.optionalDependencies) { normalizedData.optionalDependencies = {}; } - normalizedData.optionalDependencies[dependency.name] = dependency.version; + + normalizedData.optionalDependencies[name] = version; break; - case DependencyType.Peer: + } + + case DependencyType.Peer: { if (!normalizedData.peerDependencies) { normalizedData.peerDependencies = {}; } - normalizedData.peerDependencies[dependency.name] = dependency.version; + + normalizedData.peerDependencies[name] = version; break; + } + case DependencyType.Dev: // uses this._devDependencies instead case DependencyType.YarnResolutions: // uses this._resolutions instead - default: + default: { throw new InternalError('Unsupported DependencyType'); + } } } const devDependenciesKeys: string[] = [...this._devDependencies.keys()].sort(); - for (const packageName of devDependenciesKeys) { - const dependency: PackageJsonDependency = this._devDependencies.get(packageName)!; + const { name, version }: PackageJsonDependency = this._devDependencies.get(packageName)!; if (!normalizedData.devDependencies) { normalizedData.devDependencies = {}; } - normalizedData.devDependencies[dependency.name] = dependency.version; + + normalizedData.devDependencies[name] = version; } // (Do not sort this._resolutions because order may be significant; the RFC is unclear about that.) for (const packageName of this._resolutions.keys()) { - const dependency: PackageJsonDependency = this._resolutions.get(packageName)!; + const { name, version }: PackageJsonDependency = this._resolutions.get(packageName)!; if (!normalizedData.resolutions) { normalizedData.resolutions = {}; } - normalizedData.resolutions[dependency.name] = dependency.version; + + normalizedData.resolutions[name] = version; } return normalizedData; diff --git a/libraries/rush-lib/src/api/SaveCallbackPackageJsonEditor.ts b/libraries/rush-lib/src/api/SaveCallbackPackageJsonEditor.ts index 1462d824548..5b755212eab 100644 --- a/libraries/rush-lib/src/api/SaveCallbackPackageJsonEditor.ts +++ b/libraries/rush-lib/src/api/SaveCallbackPackageJsonEditor.ts @@ -24,8 +24,8 @@ export class SaveCallbackPackageJsonEditor extends PackageJsonEditor { return new SaveCallbackPackageJsonEditor(options); } - public saveIfModified(): boolean { - const modified: boolean = super.saveIfModified(); + public async saveIfModifiedAsync(): Promise { + const modified: boolean = await super.saveIfModifiedAsync(); if (this._onSaved) { this._onSaved(this.saveToObject()); } diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index 624b1a33bd6..7f2feddb836 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -325,6 +325,7 @@ export class Subspace { this._rushConfiguration ); } + return this._commonVersionsConfiguration; } diff --git a/libraries/rush-lib/src/api/VersionPolicy.ts b/libraries/rush-lib/src/api/VersionPolicy.ts index f2df9deaf58..b738ba487d0 100644 --- a/libraries/rush-lib/src/api/VersionPolicy.ts +++ b/libraries/rush-lib/src/api/VersionPolicy.ts @@ -52,6 +52,66 @@ export enum VersionPolicyDefinitionName { 'individualVersion' } +/** + * Updates the dependencies in the package json editor to values used for publishing, if needed. + * + * @returns the updated package json editor if the version format for publish is 'exact', otherwise undefined. + */ +function updateDependenciesBeforePublish( + packageName: string, + configuration: RushConfiguration, + versionFormatForPublish: VersionFormatForPublish +): PackageJsonEditor | undefined { + if (versionFormatForPublish === 'exact') { + const project: RushConfigurationProject = configuration.getProjectByName(packageName)!; + + const packageJsonEditor: PackageJsonEditor = project.packageJsonEditor; + + for (const dependency of packageJsonEditor.dependencyList) { + const rushDependencyProject: RushConfigurationProject | undefined = configuration.getProjectByName( + dependency.name + ); + + if (rushDependencyProject) { + const dependencyVersion: string = rushDependencyProject.packageJson.version; + + dependency.setVersion(dependencyVersion); + } + } + + return packageJsonEditor; + } +} + +/** + * Updates the dependencies in the package json editor to values used for checked-in source, if needed. + * + * @returns the updated package json editor if the version format for commit is 'wildcard', otherwise undefined. + */ +function updateDependenciesBeforeCommit( + packageName: string, + configuration: RushConfiguration, + versionFormatForCommit: VersionFormatForCommit +): PackageJsonEditor | undefined { + if (versionFormatForCommit === 'wildcard') { + const project: RushConfigurationProject = configuration.getProjectByName(packageName)!; + + const packageJsonEditor: PackageJsonEditor = project.packageJsonEditor; + + for (const dependency of packageJsonEditor.dependencyList) { + const rushDependencyProject: RushConfigurationProject | undefined = configuration.getProjectByName( + dependency.name + ); + + if (rushDependencyProject) { + dependency.setVersion('*'); + } + } + + return packageJsonEditor; + } +} + /** * This is the base class for version policy which controls how versions get bumped. * @public @@ -161,53 +221,63 @@ export abstract class VersionPolicy { public abstract validate(versionString: string, packageName: string): void; /** - * Tells the version policy to modify any dependencies in the target package - * to values used for publishing. + * @deprecated Use {@link VersionPolicy.setDependenciesBeforePublishAsync} method instead. */ public setDependenciesBeforePublish(packageName: string, configuration: RushConfiguration): void { - if (this._versionFormatForPublish === 'exact') { - const project: RushConfigurationProject = configuration.getProjectByName(packageName)!; + const packageJsonEditor: PackageJsonEditor | undefined = updateDependenciesBeforePublish( + packageName, + configuration, + this._versionFormatForPublish + ); - const packageJsonEditor: PackageJsonEditor = project.packageJsonEditor; + packageJsonEditor?.saveIfModified(); + } - for (const dependency of packageJsonEditor.dependencyList) { - const rushDependencyProject: RushConfigurationProject | undefined = configuration.getProjectByName( - dependency.name - ); + /** + * Tells the version policy to modify any dependencies in the target package + * to values used for publishing. + */ + public async setDependenciesBeforePublishAsync( + packageName: string, + configuration: RushConfiguration + ): Promise { + const packageJsonEditor: PackageJsonEditor | undefined = updateDependenciesBeforePublish( + packageName, + configuration, + this._versionFormatForPublish + ); - if (rushDependencyProject) { - const dependencyVersion: string = rushDependencyProject.packageJson.version; + await packageJsonEditor?.saveIfModifiedAsync(); + } - dependency.setVersion(dependencyVersion); - } - } + /** + * @deprecated Use {@link VersionPolicy.setDependenciesBeforeCommitAsync} method instead. + */ + public setDependenciesBeforeCommit(packageName: string, configuration: RushConfiguration): void { + const packageJsonEditor: PackageJsonEditor | undefined = updateDependenciesBeforeCommit( + packageName, + configuration, + this._versionFormatForCommit + ); - packageJsonEditor.saveIfModified(); - } + packageJsonEditor?.saveIfModified(); } /** * Tells the version policy to modify any dependencies in the target package * to values used for checked-in source. */ - public setDependenciesBeforeCommit(packageName: string, configuration: RushConfiguration): void { - if (this._versionFormatForCommit === 'wildcard') { - const project: RushConfigurationProject = configuration.getProjectByName(packageName)!; - - const packageJsonEditor: PackageJsonEditor = project.packageJsonEditor; - - for (const dependency of packageJsonEditor.dependencyList) { - const rushDependencyProject: RushConfigurationProject | undefined = configuration.getProjectByName( - dependency.name - ); - - if (rushDependencyProject) { - dependency.setVersion('*'); - } - } + public async setDependenciesBeforeCommitAsync( + packageName: string, + configuration: RushConfiguration + ): Promise { + const packageJsonEditor: PackageJsonEditor | undefined = updateDependenciesBeforeCommit( + packageName, + configuration, + this._versionFormatForCommit + ); - packageJsonEditor.saveIfModified(); - } + await packageJsonEditor?.saveIfModifiedAsync(); } } diff --git a/libraries/rush-lib/src/api/test/CommonVersionsConfiguration.test.ts b/libraries/rush-lib/src/api/test/CommonVersionsConfiguration.test.ts index 6e6b2553cd9..ec42d471194 100644 --- a/libraries/rush-lib/src/api/test/CommonVersionsConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/CommonVersionsConfiguration.test.ts @@ -5,9 +5,9 @@ import { CommonVersionsConfiguration } from '../CommonVersionsConfiguration'; import type { RushConfiguration } from '../RushConfiguration'; describe(CommonVersionsConfiguration.name, () => { - it('can load the file', () => { + it('can load the file', async () => { const filename: string = `${__dirname}/jsonFiles/common-versions.json`; - const configuration: CommonVersionsConfiguration = CommonVersionsConfiguration.loadFromFile( + const configuration: CommonVersionsConfiguration = await CommonVersionsConfiguration.loadFromFileAsync( filename, {} as RushConfiguration ); @@ -16,33 +16,39 @@ describe(CommonVersionsConfiguration.name, () => { expect(configuration.allowedAlternativeVersions.get('library-3')).toEqual(['^1.2.3']); }); - it('gets `ensureConsistentVersions` from the file if it provides that value', () => { + it('gets `ensureConsistentVersions` from the file if it provides that value', async () => { const filename: string = `${__dirname}/jsonFiles/common-versions-with-ensureConsistentVersionsTrue.json`; - const configuration: CommonVersionsConfiguration = CommonVersionsConfiguration.loadFromFile(filename, { - _ensureConsistentVersionsJsonValue: undefined, - ensureConsistentVersions: false - } as RushConfiguration); + const configuration: CommonVersionsConfiguration = await CommonVersionsConfiguration.loadFromFileAsync( + filename, + { + _ensureConsistentVersionsJsonValue: undefined, + ensureConsistentVersions: false + } as RushConfiguration + ); expect(configuration.ensureConsistentVersions).toBe(true); }); - it("gets `ensureConsistentVersions` from the rush configuration if common-versions.json doesn't provide that value", () => { + it("gets `ensureConsistentVersions` from the rush configuration if common-versions.json doesn't provide that value", async () => { const filename: string = `${__dirname}/jsonFiles/common-versions.json`; - const configuration: CommonVersionsConfiguration = CommonVersionsConfiguration.loadFromFile(filename, { - _ensureConsistentVersionsJsonValue: false, - ensureConsistentVersions: false - } as RushConfiguration); + const configuration: CommonVersionsConfiguration = await CommonVersionsConfiguration.loadFromFileAsync( + filename, + { + _ensureConsistentVersionsJsonValue: false, + ensureConsistentVersions: false + } as RushConfiguration + ); expect(configuration.ensureConsistentVersions).toBe(false); }); - it('Does not allow `ensureConsistentVersions` to be set in both rush.json and common-versions.json', () => { + it('Does not allow `ensureConsistentVersions` to be set in both rush.json and common-versions.json', async () => { const filename: string = `${__dirname}/jsonFiles/common-versions-with-ensureConsistentVersionsTrue.json`; - expect(() => - CommonVersionsConfiguration.loadFromFile(filename, { + await expect(() => + CommonVersionsConfiguration.loadFromFileAsync(filename, { _ensureConsistentVersionsJsonValue: false, ensureConsistentVersions: false } as RushConfiguration) - ).toThrowErrorMatchingSnapshot(); + ).rejects.toThrowErrorMatchingSnapshot(); }); }); diff --git a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts index f7513eb4a01..6b90c8c2a10 100644 --- a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts @@ -324,7 +324,7 @@ describe(RushConfiguration.name, () => { 'dependencyProjects before' ); project.packageJsonEditor.addOrUpdateDependency('project2', '1.0.0', DependencyType.Dev); - project.packageJsonEditor.saveIfModified(); + await project.packageJsonEditor.saveIfModifiedAsync(); expect(project.packageJson.devDependencies).toMatchSnapshot('devDependencies after'); expect(Array.from(project.dependencyProjects.values()).map((x) => x.packageName)).toMatchSnapshot( 'dependencyProjects after' diff --git a/libraries/rush-lib/src/api/test/VersionMismatchFinder.test.ts b/libraries/rush-lib/src/api/test/VersionMismatchFinder.test.ts index b2a661d23ce..a9ea220ec72 100644 --- a/libraries/rush-lib/src/api/test/VersionMismatchFinder.test.ts +++ b/libraries/rush-lib/src/api/test/VersionMismatchFinder.test.ts @@ -415,7 +415,7 @@ describe(VersionMismatchFinder.name, () => { expect(mismatchFinder.getMismatches()).toHaveLength(0); }); - it('handles the common-versions.json file correctly', () => { + it('handles the common-versions.json file correctly', async () => { const projectA: VersionMismatchFinderEntity = new VersionMismatchFinderProject({ packageName: 'A', packageJsonEditor: PackageJsonEditor.fromObject( @@ -429,8 +429,10 @@ describe(VersionMismatchFinder.name, () => { ), decoupledLocalDependencies: new Set() } as any as RushConfigurationProject); + const commonVersionsConfiguration: CommonVersionsConfiguration = + await CommonVersionsConfiguration.loadFromFileAsync(`${__dirname}/jsonFiles/common-versions.json`); const projectB: VersionMismatchFinderEntity = new VersionMismatchFinderCommonVersions( - CommonVersionsConfiguration.loadFromFile(`${__dirname}/jsonFiles/common-versions.json`) + commonVersionsConfiguration ); const mismatchFinder: VersionMismatchFinder = new VersionMismatchFinder([projectA, projectB]); diff --git a/libraries/rush-lib/src/cli/actions/PublishAction.ts b/libraries/rush-lib/src/cli/actions/PublishAction.ts index cb05204508f..87281a4a58e 100644 --- a/libraries/rush-lib/src/cli/actions/PublishAction.ts +++ b/libraries/rush-lib/src/cli/actions/PublishAction.ts @@ -10,7 +10,7 @@ import type { CommandLineStringParameter, CommandLineChoiceParameter } from '@rushstack/ts-command-line'; -import { FileSystem } from '@rushstack/node-core-library'; +import { Async, FileSystem } from '@rushstack/node-core-library'; import { Colorize } from '@rushstack/terminal'; import { type IChangeInfo, ChangeType } from '../../api/ChangeManagement'; @@ -24,12 +24,12 @@ import { ChangeManager } from '../../logic/ChangeManager'; import { BaseRushAction } from './BaseRushAction'; import { PublishGit } from '../../logic/PublishGit'; import * as PolicyValidator from '../../logic/policy/PolicyValidator'; -import type { VersionPolicy } from '../../api/VersionPolicy'; import { DEFAULT_PACKAGE_UPDATE_MESSAGE } from './VersionAction'; import { Utilities } from '../../utilities/Utilities'; import { Git } from '../../logic/Git'; import { RushConstants } from '../../logic/RushConstants'; import { IS_WINDOWS } from '../../utilities/executionUtilities'; +import type { RushConfiguration } from '../../api/RushConfiguration'; export class PublishAction extends BaseRushAction { private readonly _addCommitDetails: CommandLineFlagParameter; @@ -294,13 +294,13 @@ export class PublishAction extends BaseRushAction { // Make changes in temp branch. await publishGit.checkoutAsync(tempBranchName, true); - this._setDependenciesBeforePublish(); + await this._setDependenciesBeforePublishAsync(); // Make changes to package.json and change logs. changeManager.apply(this._apply.value); await changeManager.updateChangelogAsync(this._apply.value); - this._setDependenciesBeforeCommit(); + await this._setDependenciesBeforeCommitAsync(); if (await git.hasUncommittedChangesAsync()) { // Stage, commit, and push the changes to remote temp branch. @@ -311,7 +311,7 @@ export class PublishAction extends BaseRushAction { ); await publishGit.pushAsync(tempBranchName, !this._ignoreGitHooksParameter.value); - this._setDependenciesBeforePublish(); + await this._setDependenciesBeforePublishAsync(); // Override tag parameter if there is a hotfix change. for (const change of orderedChanges) { @@ -339,7 +339,7 @@ export class PublishAction extends BaseRushAction { } } - this._setDependenciesBeforeCommit(); + await this._setDependenciesBeforeCommitAsync(); // Create and push appropriate Git tags. await this._gitAddTagsAsync(publishGit, orderedChanges); @@ -560,28 +560,30 @@ export class PublishAction extends BaseRushAction { } } - private _setDependenciesBeforePublish(): void { - for (const project of this.rushConfiguration.projects) { - if (!this._versionPolicy.value || this._versionPolicy.value === project.versionPolicyName) { - const versionPolicy: VersionPolicy | undefined = project.versionPolicy; - - if (versionPolicy) { - versionPolicy.setDependenciesBeforePublish(project.packageName, this.rushConfiguration); + private async _setDependenciesBeforePublishAsync(): Promise { + const rushConfiguration: RushConfiguration = this.rushConfiguration; + await Async.forEachAsync( + rushConfiguration.projects, + async ({ versionPolicy, versionPolicyName, packageName }) => { + if (!this._versionPolicy.value || this._versionPolicy.value === versionPolicyName) { + await versionPolicy?.setDependenciesBeforePublishAsync(packageName, rushConfiguration); } - } - } + }, + { concurrency: 10 } + ); } - private _setDependenciesBeforeCommit(): void { - for (const project of this.rushConfiguration.projects) { - if (!this._versionPolicy.value || this._versionPolicy.value === project.versionPolicyName) { - const versionPolicy: VersionPolicy | undefined = project.versionPolicy; - - if (versionPolicy) { - versionPolicy.setDependenciesBeforePublish(project.packageName, this.rushConfiguration); + private async _setDependenciesBeforeCommitAsync(): Promise { + const rushConfiguration: RushConfiguration = this.rushConfiguration; + await Async.forEachAsync( + rushConfiguration.projects, + async ({ versionPolicy, versionPolicyName, packageName }) => { + if (!this._versionPolicy.value || this._versionPolicy.value === versionPolicyName) { + await versionPolicy?.setDependenciesBeforeCommitAsync(packageName, rushConfiguration); } - } - } + }, + { concurrency: 10 } + ); } private _addNpmPublishHome(supportEnvVarFallbackSyntax: boolean): void { diff --git a/libraries/rush-lib/src/logic/Autoinstaller.ts b/libraries/rush-lib/src/logic/Autoinstaller.ts index 8c35ea07b82..ae55a8c361b 100644 --- a/libraries/rush-lib/src/logic/Autoinstaller.ts +++ b/libraries/rush-lib/src/logic/Autoinstaller.ts @@ -210,7 +210,7 @@ export class Autoinstaller { } // Detect a common mistake where PNPM prints "Already up-to-date" without creating a shrinkwrap file - const packageJsonEditor: PackageJsonEditor = PackageJsonEditor.load(this.packageJsonPath); + const packageJsonEditor: PackageJsonEditor = await PackageJsonEditor.loadAsync(this.packageJsonPath); if (packageJsonEditor.dependencyList.length === 0) { throw new Error( 'You must add at least one dependency to the autoinstaller package' + diff --git a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts index 762dce515ab..3a1e4789fbb 100644 --- a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts @@ -5,6 +5,7 @@ import * as semver from 'semver'; import type { INpmCheckPackageSummary } from '@rushstack/npm-check-fork'; import { Colorize, type ITerminal } from '@rushstack/terminal'; +import { Async } from '@rushstack/node-core-library'; import type { RushConfiguration } from '../api/RushConfiguration'; import type { BaseInstallManager } from './base/BaseInstallManager'; @@ -223,11 +224,16 @@ export class PackageJsonUpdater { } } - for (const [filePath, project] of allPackageUpdates) { - if (project.saveIfModified()) { - this._terminal.writeLine(Colorize.green('Wrote ') + filePath); - } - } + await Async.forEachAsync( + allPackageUpdates, + async ([filePath, project]) => { + const modified: boolean = await project.saveIfModifiedAsync(); + if (modified) { + this._terminal.writeLine(Colorize.green('Wrote ') + filePath); + } + }, + { concurrency: 10 } + ); if (!skipUpdate) { if (this._rushConfiguration.subspacesFeatureEnabled) { @@ -252,12 +258,18 @@ export class PackageJsonUpdater { } else { throw new Error('only accept "rush add" or "rush remove"'); } + const { skipUpdate, debugInstall, variant } = options; - for (const { project } of allPackageUpdates) { - if (project.saveIfModified()) { - this._terminal.writeLine(Colorize.green('Wrote'), project.filePath); - } - } + await Async.forEachAsync( + allPackageUpdates, + async ({ project }) => { + const modified: boolean = await project.saveIfModifiedAsync(); + if (modified) { + this._terminal.writeLine(Colorize.green('Wrote'), project.filePath); + } + }, + { concurrency: 10 } + ); if (!skipUpdate) { if (this._rushConfiguration.subspacesFeatureEnabled) { diff --git a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts index 60e9dcc2971..247aa96131f 100644 --- a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -357,7 +357,8 @@ export class RushInstallManager extends BaseInstallManager { } // Save the package.json if we modified the version references and warn that the package.json was modified - if (packageJson.saveIfModified()) { + const modified: boolean = await packageJson.saveIfModifiedAsync(); + if (modified) { // eslint-disable-next-line no-console console.log( Colorize.yellow( diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 4be7bb4a913..9526f0e8d59 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -316,7 +316,8 @@ export class WorkspaceInstallManager extends BaseInstallManager { } // Save the package.json if we modified the version references and warn that the package.json was modified - if (packageJson.saveIfModified()) { + const modified: boolean = await packageJson.saveIfModifiedAsync(); + if (modified) { // eslint-disable-next-line no-console console.log( Colorize.yellow( diff --git a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderCommonVersions.ts b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderCommonVersions.ts index bec3f535149..1e2c11b17bf 100644 --- a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderCommonVersions.ts +++ b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderCommonVersions.ts @@ -63,8 +63,8 @@ export class VersionMismatchFinderCommonVersions extends VersionMismatchFinderEn throw new Error('Not supported.'); } - public saveIfModified(): boolean { - return this._fileManager.save(); + public async saveIfModifiedAsync(): Promise { + return await this._fileManager.saveAsync(); } private _getPackageJsonDependency(dependencyName: string, version: string): PackageJsonDependency { diff --git a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderEntity.ts b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderEntity.ts index 054d7291c79..5b2284d97aa 100644 --- a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderEntity.ts +++ b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderEntity.ts @@ -15,9 +15,10 @@ export abstract class VersionMismatchFinderEntity { public readonly skipRushCheck: boolean | undefined; public constructor(options: IVersionMismatchFinderEntityOptions) { - this.friendlyName = options.friendlyName; - this.decoupledLocalDependencies = options.decoupledLocalDependencies; - this.skipRushCheck = options.skipRushCheck; + const { friendlyName, decoupledLocalDependencies, skipRushCheck } = options; + this.friendlyName = friendlyName; + this.decoupledLocalDependencies = decoupledLocalDependencies; + this.skipRushCheck = skipRushCheck; } public abstract get filePath(): string; @@ -31,5 +32,5 @@ export abstract class VersionMismatchFinderEntity { dependencyType: DependencyType ): void; public abstract removeDependency(packageName: string, dependencyType: DependencyType): void; - public abstract saveIfModified(): boolean; + public abstract saveIfModifiedAsync(): Promise; } diff --git a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderProject.ts b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderProject.ts index 1bbfd4d7a84..cc63370b687 100644 --- a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderProject.ts +++ b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinderProject.ts @@ -48,7 +48,7 @@ export class VersionMismatchFinderProject extends VersionMismatchFinderEntity { return this._fileManager.removeDependency(packageName, dependencyType); } - public saveIfModified(): boolean { - return this._fileManager.saveIfModified(); + public async saveIfModifiedAsync(): Promise { + return await this._fileManager.saveIfModifiedAsync(); } } diff --git a/repo-scripts/repo-toolbox/src/cli/actions/BumpDecoupledLocalDependencies.ts b/repo-scripts/repo-toolbox/src/cli/actions/BumpDecoupledLocalDependencies.ts index 14657e0be70..ef70cefc537 100644 --- a/repo-scripts/repo-toolbox/src/cli/actions/BumpDecoupledLocalDependencies.ts +++ b/repo-scripts/repo-toolbox/src/cli/actions/BumpDecoupledLocalDependencies.ts @@ -94,7 +94,7 @@ export class BumpDecoupledLocalDependencies extends CommandLineAction { const autoinstallerName: string = folderItem.name; const packageJsonPath: string = `${commonAutoinstallersFolder}/${autoinstallerName}/package.json`; try { - const packageJsonEditor: PackageJsonEditor = PackageJsonEditor.load(packageJsonPath); + const packageJsonEditor: PackageJsonEditor = await PackageJsonEditor.loadAsync(packageJsonPath); const { dependencyList, devDependencyList } = packageJsonEditor; const decoupledLocalDependencies: Set = new Set(); @@ -135,40 +135,45 @@ export class BumpDecoupledLocalDependencies extends CommandLineAction { terminal.writeLine(); - for (const { packageName, decoupledLocalDependencies, subspace, packageJsonEditor } of projectsToUpdate) { - const { allowedAlternativeVersions } = subspace?.getCommonVersions() ?? {}; - - for (const cyclicDependencyProject of decoupledLocalDependencies) { - const { version: existingVersion } = - packageJsonEditor.tryGetDependency(cyclicDependencyProject) ?? - packageJsonEditor.tryGetDevDependency(cyclicDependencyProject) ?? - {}; - if ( - existingVersion && - allowedAlternativeVersions?.get(cyclicDependencyProject)?.includes(existingVersion) - ) { - // Skip if the existing version is allowed by common-versions.json - continue; - } + await Async.forEachAsync( + projectsToUpdate, + async ({ packageName, decoupledLocalDependencies, subspace, packageJsonEditor }) => { + const { allowedAlternativeVersions } = subspace?.getCommonVersions() ?? {}; + + for (const cyclicDependencyProject of decoupledLocalDependencies) { + const { version: existingVersion } = + packageJsonEditor.tryGetDependency(cyclicDependencyProject) ?? + packageJsonEditor.tryGetDevDependency(cyclicDependencyProject) ?? + {}; + if ( + existingVersion && + allowedAlternativeVersions?.get(cyclicDependencyProject)?.includes(existingVersion) + ) { + // Skip if the existing version is allowed by common-versions.json + continue; + } - const newVersion: string = decoupledLocalDependencyVersionsByName.get(cyclicDependencyProject)!; - if (packageJsonEditor.tryGetDependency(cyclicDependencyProject)) { - packageJsonEditor.addOrUpdateDependency( - cyclicDependencyProject, - newVersion, - DependencyType.Regular - ); - } + const newVersion: string = decoupledLocalDependencyVersionsByName.get(cyclicDependencyProject)!; + if (packageJsonEditor.tryGetDependency(cyclicDependencyProject)) { + packageJsonEditor.addOrUpdateDependency( + cyclicDependencyProject, + newVersion, + DependencyType.Regular + ); + } - if (packageJsonEditor.tryGetDevDependency(cyclicDependencyProject)) { - packageJsonEditor.addOrUpdateDependency(cyclicDependencyProject, newVersion, DependencyType.Dev); + if (packageJsonEditor.tryGetDevDependency(cyclicDependencyProject)) { + packageJsonEditor.addOrUpdateDependency(cyclicDependencyProject, newVersion, DependencyType.Dev); + } } - } - if (packageJsonEditor.saveIfModified()) { - terminal.writeLine(`Updated ${packageName}`); - } - } + const modified: boolean = await packageJsonEditor.saveIfModifiedAsync(); + if (modified) { + terminal.writeLine(`Updated ${packageName}`); + } + }, + { concurrency: 10 } + ); terminal.writeLine();