From cba38b3a7b30642c587b1712da156c4f8f25ecf4 Mon Sep 17 00:00:00 2001 From: "chenzhelong.sirius" Date: Tue, 23 Jun 2026 13:30:19 +0800 Subject: [PATCH 1/6] fix: avoid unnecessary package manager install lock --- ...manager-install-lock_2026-06-23-13-27.json | 11 ++ .../logic/installManager/InstallHelpers.ts | 111 +++++++++++++----- .../src/logic/test/InstallHelpers.test.ts | 74 +++++++++++- 3 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 common/changes/@microsoft/rush/package-manager-install-lock_2026-06-23-13-27.json diff --git a/common/changes/@microsoft/rush/package-manager-install-lock_2026-06-23-13-27.json b/common/changes/@microsoft/rush/package-manager-install-lock_2026-06-23-13-27.json new file mode 100644 index 0000000000..b11b30f952 --- /dev/null +++ b/common/changes/@microsoft/rush/package-manager-install-lock_2026-06-23-13-27.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Avoid acquiring the global package manager install lock when the requested package manager is already installed and no install lock file exists.", + "type": "patch" + } + ], + "packageName": "@microsoft/rush", + "email": "EscapeB@users.noreply.github.com" +} diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 4c74ba1a5d..0a888e89a2 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -424,47 +424,81 @@ export class InstallHelpers { node: process.versions.node }); + if ( + (await packageManagerMarker.isValidAsync()) && + !InstallHelpers._doesPackageManagerInstallLockFileExist(rushUserFolder, packageManagerAndVersion) + ) { + logIfConsoleOutputIsNotRestricted( + `Found ${packageManager} version ${packageManagerVersion} in ${packageManagerToolFolder}` + ); + InstallHelpers._ensureLocalPackageManagerSymlink( + rushConfiguration, + packageManager, + packageManagerToolFolder, + logIfConsoleOutputIsNotRestricted + ); + return; + } + logIfConsoleOutputIsNotRestricted(`Trying to acquire lock for ${packageManagerAndVersion}`); const lock: LockFile = await LockFile.acquireAsync(rushUserFolder, packageManagerAndVersion); logIfConsoleOutputIsNotRestricted(`Acquired lock for ${packageManagerAndVersion}`); - if (!(await packageManagerMarker.isValidAsync()) || lock.dirtyWhenAcquired) { - logIfConsoleOutputIsNotRestricted( - Colorize.bold(`Installing ${packageManager} version ${packageManagerVersion}\n`) - ); + try { + if (!(await packageManagerMarker.isValidAsync()) || lock.dirtyWhenAcquired) { + logIfConsoleOutputIsNotRestricted( + Colorize.bold(`Installing ${packageManager} version ${packageManagerVersion}\n`) + ); - // note that this will remove the last-install flag from the directory - await Utilities.installPackageInDirectoryAsync({ - directory: packageManagerToolFolder, - packageName: packageManager, - version: rushConfiguration.packageManagerToolVersion, - tempPackageTitle: `${packageManager}-local-install`, - maxInstallAttempts: maxInstallAttempts, - // This is using a local configuration to install a package in a shared global location. - // Generally that's a bad practice, but in this case if we can successfully install - // the package at all, we can reasonably assume it's good for all the repositories. - // In particular, we'll assume that two different NPM registries cannot have two - // different implementations of the same version of the same package. - // This was needed for: https://github.com/microsoft/rushstack/issues/691 - commonRushConfigFolder: rushConfiguration.commonRushConfigFolder, - // Only filter npm-incompatible properties when the repo uses pnpm or yarn. - // If the repo uses npm, the .npmrc is already configured for npm, so don't filter. - filterNpmIncompatibleProperties: rushConfiguration.packageManager !== 'npm' - }); + // note that this will remove the last-install flag from the directory + await Utilities.installPackageInDirectoryAsync({ + directory: packageManagerToolFolder, + packageName: packageManager, + version: rushConfiguration.packageManagerToolVersion, + tempPackageTitle: `${packageManager}-local-install`, + maxInstallAttempts: maxInstallAttempts, + // This is using a local configuration to install a package in a shared global location. + // Generally that's a bad practice, but in this case if we can successfully install + // the package at all, we can reasonably assume it's good for all the repositories. + // In particular, we'll assume that two different NPM registries cannot have two + // different implementations of the same version of the same package. + // This was needed for: https://github.com/microsoft/rushstack/issues/691 + commonRushConfigFolder: rushConfiguration.commonRushConfigFolder, + // Only filter npm-incompatible properties when the repo uses pnpm or yarn. + // If the repo uses npm, the .npmrc is already configured for npm, so don't filter. + filterNpmIncompatibleProperties: rushConfiguration.packageManager !== 'npm' + }); + + logIfConsoleOutputIsNotRestricted( + `Successfully installed ${packageManager} version ${packageManagerVersion}` + ); + } else { + logIfConsoleOutputIsNotRestricted( + `Found ${packageManager} version ${packageManagerVersion} in ${packageManagerToolFolder}` + ); + } - logIfConsoleOutputIsNotRestricted( - `Successfully installed ${packageManager} version ${packageManagerVersion}` - ); - } else { - logIfConsoleOutputIsNotRestricted( - `Found ${packageManager} version ${packageManagerVersion} in ${packageManagerToolFolder}` + await packageManagerMarker.createAsync(); + + InstallHelpers._ensureLocalPackageManagerSymlink( + rushConfiguration, + packageManager, + packageManagerToolFolder, + logIfConsoleOutputIsNotRestricted ); + } finally { + lock.release(); } + } - await packageManagerMarker.createAsync(); - + private static _ensureLocalPackageManagerSymlink( + rushConfiguration: RushConfiguration, + packageManager: PackageManagerName, + packageManagerToolFolder: string, + logIfConsoleOutputIsNotRestricted: (message?: string) => void + ): void { // Example: "C:\MyRepo\common\temp" FileSystem.ensureFolder(rushConfiguration.commonTempFolder); @@ -488,8 +522,23 @@ export class InstallHelpers { linkTargetPath: packageManagerToolFolder, newLinkPath: localPackageManagerToolFolder }); + } + + private static _doesPackageManagerInstallLockFileExist( + rushUserFolder: string, + packageManagerAndVersion: string + ): boolean { + for (const itemName of FileSystem.readFolderItemNames(rushUserFolder)) { + if (itemName === `${packageManagerAndVersion}.lock`) { + return true; + } + + if (itemName.startsWith(`${packageManagerAndVersion}#`) && itemName.endsWith('.lock')) { + return true; + } + } - lock.release(); + return false; } // Helper for getPackageManagerEnvironment diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index b766db92f1..6c1396dc03 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -1,11 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { type IPackageJson, JsonFile } from '@rushstack/node-core-library'; +import * as path from 'node:path'; + +import { FileSystem, type IPackageJson, JsonFile, LockFile } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; import { InstallHelpers } from '../installManager/InstallHelpers'; import { RushConfiguration } from '../../api/RushConfiguration'; +import { LastInstallFlag } from '../../api/LastInstallFlag'; +import type { RushGlobalFolder } from '../../api/RushGlobalFolder'; +import { Utilities } from '../../utilities/Utilities'; describe(InstallHelpers.name, () => { describe(InstallHelpers.generateCommonPackageJsonAsync.name, () => { @@ -114,4 +119,71 @@ describe(InstallHelpers.name, () => { expect(pnpmField).not.toHaveProperty('patchedDependencies'); }); }); + + describe(InstallHelpers.ensureLocalPackageManagerAsync.name, () => { + const tempFolderPath: string = `${__dirname}/temp/${InstallHelpers.name}`; + + beforeEach(() => { + FileSystem.ensureEmptyFolder(tempFolderPath); + }); + + afterEach(() => { + FileSystem.deleteFolder(tempFolderPath); + jest.restoreAllMocks(); + }); + + it('does not acquire the global lock when the package manager is already installed', async () => { + const rushGlobalFolder: RushGlobalFolder = { + path: `${tempFolderPath}/rush-global`, + nodeSpecificPath: `${tempFolderPath}/rush-global/node-${process.version}` + } as RushGlobalFolder; + const packageManagerToolFolder: string = path.join(rushGlobalFolder.nodeSpecificPath, 'pnpm-10.27.0'); + await new LastInstallFlag(packageManagerToolFolder, { node: process.versions.node }).createAsync(); + + const lockAcquireSpy: jest.SpyInstance = jest.spyOn(LockFile, 'acquireAsync'); + const rushConfiguration: RushConfiguration = { + commonRushConfigFolder: `${tempFolderPath}/common/config/rush`, + commonTempFolder: `${tempFolderPath}/common/temp`, + packageManager: 'pnpm', + packageManagerToolVersion: '10.27.0' + } as RushConfiguration; + + await InstallHelpers.ensureLocalPackageManagerAsync(rushConfiguration, rushGlobalFolder, 1, true); + + expect(lockAcquireSpy).not.toHaveBeenCalled(); + expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); + }); + + it('acquires the global lock if an install lock file is present', async () => { + const rushGlobalFolder: RushGlobalFolder = { + path: `${tempFolderPath}/rush-global`, + nodeSpecificPath: `${tempFolderPath}/rush-global/node-${process.version}` + } as RushGlobalFolder; + const packageManagerToolFolder: string = path.join(rushGlobalFolder.nodeSpecificPath, 'pnpm-10.27.0'); + await new LastInstallFlag(packageManagerToolFolder, { node: process.versions.node }).createAsync(); + FileSystem.writeFile(`${rushGlobalFolder.nodeSpecificPath}/pnpm-10.27.0#123.lock`, ''); + + const releaseAsync: jest.Mock = jest.fn(); + const lockAcquireSpy: jest.SpyInstance = jest.spyOn(LockFile, 'acquireAsync').mockResolvedValue({ + dirtyWhenAcquired: true, + release: releaseAsync + } as unknown as LockFile); + const installSpy: jest.SpyInstance = jest + .spyOn(Utilities, 'installPackageInDirectoryAsync') + .mockResolvedValue(); + const rushConfiguration: RushConfiguration = { + commonRushConfigFolder: `${tempFolderPath}/common/config/rush`, + commonTempFolder: `${tempFolderPath}/common/temp`, + packageManager: 'pnpm', + packageManagerToolVersion: '10.27.0' + } as RushConfiguration; + + await InstallHelpers.ensureLocalPackageManagerAsync(rushConfiguration, rushGlobalFolder, 1, true); + + expect(lockAcquireSpy).toHaveBeenCalledTimes(1); + expect(installSpy).toHaveBeenCalledTimes(1); + expect(releaseAsync).toHaveBeenCalledTimes(1); + expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); + }); + }); }); From 515082e92f5300ee56cc4285dad08a7a4be35283 Mon Sep 17 00:00:00 2001 From: "chenzhelong.sirius" Date: Tue, 30 Jun 2026 13:38:47 +0800 Subject: [PATCH 2/6] test: improve package manager install lock coverage --- .../src/logic/test/InstallHelpers.test.ts | 104 +++++++++++++----- 1 file changed, 75 insertions(+), 29 deletions(-) diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 6c1396dc03..52fd8141cf 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -122,6 +122,61 @@ describe(InstallHelpers.name, () => { describe(InstallHelpers.ensureLocalPackageManagerAsync.name, () => { const tempFolderPath: string = `${__dirname}/temp/${InstallHelpers.name}`; + const packageManager: 'pnpm' = 'pnpm'; + const packageManagerVersion: string = '10.27.0'; + + function getRushGlobalFolder(): RushGlobalFolder { + return { + path: `${tempFolderPath}/rush-global`, + nodeSpecificPath: `${tempFolderPath}/rush-global/node-${process.version}` + } as RushGlobalFolder; + } + + function getRushConfiguration(): RushConfiguration { + return { + commonRushConfigFolder: `${tempFolderPath}/common/config/rush`, + commonTempFolder: `${tempFolderPath}/common/temp`, + packageManager, + packageManagerToolVersion: packageManagerVersion + } as RushConfiguration; + } + + function getPackageManagerToolFolder(rushGlobalFolder: RushGlobalFolder): string { + return path.join(rushGlobalFolder.nodeSpecificPath, `${packageManager}-${packageManagerVersion}`); + } + + async function writeInstalledPackageManagerAsync(rushGlobalFolder: RushGlobalFolder): Promise { + const packageManagerToolFolder: string = getPackageManagerToolFolder(rushGlobalFolder); + + JsonFile.save( + { + dependencies: { + [packageManager]: packageManagerVersion + }, + description: 'Temporary file generated by the Rush tool', + name: `${packageManager}-local-install`, + private: true, + version: '0.0.0' + }, + path.join(packageManagerToolFolder, 'package.json'), + { ensureFolderExists: true } + ); + + JsonFile.save( + { + name: packageManager, + version: packageManagerVersion + }, + path.join(packageManagerToolFolder, 'node_modules', packageManager, 'package.json'), + { ensureFolderExists: true } + ); + + const packageManagerBinFolder: string = path.join(packageManagerToolFolder, 'node_modules', '.bin'); + FileSystem.ensureFolder(packageManagerBinFolder); + FileSystem.writeFile(path.join(packageManagerBinFolder, packageManager), ''); + + await new LastInstallFlag(packageManagerToolFolder, { node: process.versions.node }).createAsync(); + } beforeEach(() => { FileSystem.ensureEmptyFolder(tempFolderPath); @@ -133,56 +188,47 @@ describe(InstallHelpers.name, () => { }); it('does not acquire the global lock when the package manager is already installed', async () => { - const rushGlobalFolder: RushGlobalFolder = { - path: `${tempFolderPath}/rush-global`, - nodeSpecificPath: `${tempFolderPath}/rush-global/node-${process.version}` - } as RushGlobalFolder; - const packageManagerToolFolder: string = path.join(rushGlobalFolder.nodeSpecificPath, 'pnpm-10.27.0'); - await new LastInstallFlag(packageManagerToolFolder, { node: process.versions.node }).createAsync(); + const rushGlobalFolder: RushGlobalFolder = getRushGlobalFolder(); + await writeInstalledPackageManagerAsync(rushGlobalFolder); - const lockAcquireSpy: jest.SpyInstance = jest.spyOn(LockFile, 'acquireAsync'); - const rushConfiguration: RushConfiguration = { - commonRushConfigFolder: `${tempFolderPath}/common/config/rush`, - commonTempFolder: `${tempFolderPath}/common/temp`, - packageManager: 'pnpm', - packageManagerToolVersion: '10.27.0' - } as RushConfiguration; + const lockAcquireSpy: jest.SpyInstance = jest + .spyOn(LockFile, 'acquireAsync') + .mockResolvedValue(false as unknown as LockFile); + const installSpy: jest.SpyInstance = jest + .spyOn(Utilities, 'installPackageInDirectoryAsync') + .mockRejectedValue(new Error('The package manager should already be installed.')); + const rushConfiguration: RushConfiguration = getRushConfiguration(); await InstallHelpers.ensureLocalPackageManagerAsync(rushConfiguration, rushGlobalFolder, 1, true); expect(lockAcquireSpy).not.toHaveBeenCalled(); + expect(installSpy).not.toHaveBeenCalled(); expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); }); it('acquires the global lock if an install lock file is present', async () => { - const rushGlobalFolder: RushGlobalFolder = { - path: `${tempFolderPath}/rush-global`, - nodeSpecificPath: `${tempFolderPath}/rush-global/node-${process.version}` - } as RushGlobalFolder; - const packageManagerToolFolder: string = path.join(rushGlobalFolder.nodeSpecificPath, 'pnpm-10.27.0'); - await new LastInstallFlag(packageManagerToolFolder, { node: process.versions.node }).createAsync(); - FileSystem.writeFile(`${rushGlobalFolder.nodeSpecificPath}/pnpm-10.27.0#123.lock`, ''); + const rushGlobalFolder: RushGlobalFolder = getRushGlobalFolder(); + await writeInstalledPackageManagerAsync(rushGlobalFolder); + FileSystem.writeFile( + path.join(rushGlobalFolder.nodeSpecificPath, `${packageManager}-${packageManagerVersion}#123.lock`), + '' + ); - const releaseAsync: jest.Mock = jest.fn(); + const releaseLockMock: jest.Mock = jest.fn(); const lockAcquireSpy: jest.SpyInstance = jest.spyOn(LockFile, 'acquireAsync').mockResolvedValue({ dirtyWhenAcquired: true, - release: releaseAsync + release: releaseLockMock } as unknown as LockFile); const installSpy: jest.SpyInstance = jest .spyOn(Utilities, 'installPackageInDirectoryAsync') .mockResolvedValue(); - const rushConfiguration: RushConfiguration = { - commonRushConfigFolder: `${tempFolderPath}/common/config/rush`, - commonTempFolder: `${tempFolderPath}/common/temp`, - packageManager: 'pnpm', - packageManagerToolVersion: '10.27.0' - } as RushConfiguration; + const rushConfiguration: RushConfiguration = getRushConfiguration(); await InstallHelpers.ensureLocalPackageManagerAsync(rushConfiguration, rushGlobalFolder, 1, true); expect(lockAcquireSpy).toHaveBeenCalledTimes(1); expect(installSpy).toHaveBeenCalledTimes(1); - expect(releaseAsync).toHaveBeenCalledTimes(1); + expect(releaseLockMock).toHaveBeenCalledTimes(1); expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); }); }); From 238b3fe89b7bfe110f314207ee41c9ef66b2411f Mon Sep 17 00:00:00 2001 From: EscapeB <463355954@qq.com> Date: Mon, 20 Jul 2026 16:13:36 +0800 Subject: [PATCH 3/6] Update libraries/rush-lib/src/logic/test/InstallHelpers.test.ts Co-authored-by: Ian Clanton-Thuon --- libraries/rush-lib/src/logic/test/InstallHelpers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 52fd8141cf..1daa3a084e 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -148,7 +148,7 @@ describe(InstallHelpers.name, () => { async function writeInstalledPackageManagerAsync(rushGlobalFolder: RushGlobalFolder): Promise { const packageManagerToolFolder: string = getPackageManagerToolFolder(rushGlobalFolder); - JsonFile.save( + await JsonFile.saveAsync( { dependencies: { [packageManager]: packageManagerVersion From 12393bd0a976e51cf05f0af477b4294676197902 Mon Sep 17 00:00:00 2001 From: "chenzhelong.sirius" Date: Mon, 20 Jul 2026 16:23:08 +0800 Subject: [PATCH 4/6] fix: address package manager lock review feedback --- libraries/rush-lib/src/api/LastInstallFlag.ts | 31 ++++++++- .../logic/installManager/InstallHelpers.ts | 30 +++------ .../src/logic/test/InstallHelpers.test.ts | 66 ++++++++++--------- 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/libraries/rush-lib/src/api/LastInstallFlag.ts b/libraries/rush-lib/src/api/LastInstallFlag.ts index a0af3df9dd..256663ab4a 100644 --- a/libraries/rush-lib/src/api/LastInstallFlag.ts +++ b/libraries/rush-lib/src/api/LastInstallFlag.ts @@ -3,7 +3,14 @@ import { pnpmSyncGetJsonVersion } from 'pnpm-sync-lib'; -import { JsonFile, type JsonObject, Path, type IPackageJson, Objects } from '@rushstack/node-core-library'; +import { + FileSystem, + JsonFile, + type JsonObject, + Path, + type IPackageJson, + Objects +} from '@rushstack/node-core-library'; import type { PackageManagerName } from './packageManager/PackageManager'; import type { RushConfiguration } from './RushConfiguration'; @@ -182,6 +189,28 @@ export class LastInstallFlag extends FlagFile> { } } +/** + * Checks for a LockFile associated with the same install resource as a LastInstallFlag. + * + * @internal + */ +export async function doesLastInstallFlagLockFileExistAsync( + folderPath: string, + lockFileResourceName: string +): Promise { + for (const itemName of await FileSystem.readFolderItemNamesAsync(folderPath)) { + if (itemName === `${lockFileResourceName}.lock`) { + return true; + } + + if (itemName.startsWith(`${lockFileResourceName}#`) && itemName.endsWith('.lock')) { + return true; + } + } + + return false; +} + /** * Gets the LastInstall flag and sets the current state. This state is used to compare * against the last-known-good state tracked by the LastInstall flag. diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 0a888e89a2..998c36ad83 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -12,7 +12,7 @@ import { } from '@rushstack/node-core-library'; import { Colorize, type ITerminal } from '@rushstack/terminal'; -import { LastInstallFlag } from '../../api/LastInstallFlag'; +import { doesLastInstallFlagLockFileExistAsync, LastInstallFlag } from '../../api/LastInstallFlag'; import type { PackageManagerName } from '../../api/packageManager/PackageManager'; import type { RushConfiguration } from '../../api/RushConfiguration'; import type { RushGlobalFolder } from '../../api/RushGlobalFolder'; @@ -424,10 +424,13 @@ export class InstallHelpers { node: process.versions.node }); - if ( - (await packageManagerMarker.isValidAsync()) && - !InstallHelpers._doesPackageManagerInstallLockFileExist(rushUserFolder, packageManagerAndVersion) - ) { + const isPackageManagerMarkerValid: boolean = await packageManagerMarker.isValidAsync(); + const packageManagerInstallLockFileExists: boolean = await doesLastInstallFlagLockFileExistAsync( + rushUserFolder, + packageManagerAndVersion + ); + + if (isPackageManagerMarkerValid && !packageManagerInstallLockFileExists) { logIfConsoleOutputIsNotRestricted( `Found ${packageManager} version ${packageManagerVersion} in ${packageManagerToolFolder}` ); @@ -524,23 +527,6 @@ export class InstallHelpers { }); } - private static _doesPackageManagerInstallLockFileExist( - rushUserFolder: string, - packageManagerAndVersion: string - ): boolean { - for (const itemName of FileSystem.readFolderItemNames(rushUserFolder)) { - if (itemName === `${packageManagerAndVersion}.lock`) { - return true; - } - - if (itemName.startsWith(`${packageManagerAndVersion}#`) && itemName.endsWith('.lock')) { - return true; - } - } - - return false; - } - // Helper for getPackageManagerEnvironment private static _mergeEnvironmentVariables( baseEnv: NodeJS.ProcessEnv, diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 1daa3a084e..2368803371 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'node:path'; - import { FileSystem, type IPackageJson, JsonFile, LockFile } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; @@ -142,38 +140,38 @@ describe(InstallHelpers.name, () => { } function getPackageManagerToolFolder(rushGlobalFolder: RushGlobalFolder): string { - return path.join(rushGlobalFolder.nodeSpecificPath, `${packageManager}-${packageManagerVersion}`); + return `${rushGlobalFolder.nodeSpecificPath}/${packageManager}-${packageManagerVersion}`; } async function writeInstalledPackageManagerAsync(rushGlobalFolder: RushGlobalFolder): Promise { const packageManagerToolFolder: string = getPackageManagerToolFolder(rushGlobalFolder); - await JsonFile.saveAsync( - { - dependencies: { - [packageManager]: packageManagerVersion + await Promise.all([ + JsonFile.saveAsync( + { + dependencies: { + [packageManager]: packageManagerVersion + }, + description: 'Temporary file generated by the Rush tool', + name: `${packageManager}-local-install`, + private: true, + version: '0.0.0' }, - description: 'Temporary file generated by the Rush tool', - name: `${packageManager}-local-install`, - private: true, - version: '0.0.0' - }, - path.join(packageManagerToolFolder, 'package.json'), - { ensureFolderExists: true } - ); - - JsonFile.save( - { - name: packageManager, - version: packageManagerVersion - }, - path.join(packageManagerToolFolder, 'node_modules', packageManager, 'package.json'), - { ensureFolderExists: true } - ); - - const packageManagerBinFolder: string = path.join(packageManagerToolFolder, 'node_modules', '.bin'); - FileSystem.ensureFolder(packageManagerBinFolder); - FileSystem.writeFile(path.join(packageManagerBinFolder, packageManager), ''); + `${packageManagerToolFolder}/package.json`, + { ensureFolderExists: true } + ), + JsonFile.saveAsync( + { + name: packageManager, + version: packageManagerVersion + }, + `${packageManagerToolFolder}/node_modules/${packageManager}/package.json`, + { ensureFolderExists: true } + ), + FileSystem.writeFileAsync(`${packageManagerToolFolder}/node_modules/.bin/${packageManager}`, '', { + ensureFolderExists: true + }) + ]); await new LastInstallFlag(packageManagerToolFolder, { node: process.versions.node }).createAsync(); } @@ -203,14 +201,16 @@ describe(InstallHelpers.name, () => { expect(lockAcquireSpy).not.toHaveBeenCalled(); expect(installSpy).not.toHaveBeenCalled(); - expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); + await expect( + FileSystem.existsAsync(`${rushConfiguration.commonTempFolder}/pnpm-local`) + ).resolves.toEqual(true); }); it('acquires the global lock if an install lock file is present', async () => { const rushGlobalFolder: RushGlobalFolder = getRushGlobalFolder(); await writeInstalledPackageManagerAsync(rushGlobalFolder); - FileSystem.writeFile( - path.join(rushGlobalFolder.nodeSpecificPath, `${packageManager}-${packageManagerVersion}#123.lock`), + await FileSystem.writeFileAsync( + `${rushGlobalFolder.nodeSpecificPath}/${packageManager}-${packageManagerVersion}#123.lock`, '' ); @@ -229,7 +229,9 @@ describe(InstallHelpers.name, () => { expect(lockAcquireSpy).toHaveBeenCalledTimes(1); expect(installSpy).toHaveBeenCalledTimes(1); expect(releaseLockMock).toHaveBeenCalledTimes(1); - expect(FileSystem.exists(`${rushConfiguration.commonTempFolder}/pnpm-local`)).toEqual(true); + await expect( + FileSystem.existsAsync(`${rushConfiguration.commonTempFolder}/pnpm-local`) + ).resolves.toEqual(true); }); }); }); From 6d020d52b1552d7b2a04a0b72d3236a610213e99 Mon Sep 17 00:00:00 2001 From: "chenzhelong.sirius" Date: Mon, 20 Jul 2026 19:27:02 +0800 Subject: [PATCH 5/6] fix: await package manager symlink setup --- .../rush-lib/src/logic/installManager/InstallHelpers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 998c36ad83..6466ec8f4e 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -434,7 +434,7 @@ export class InstallHelpers { logIfConsoleOutputIsNotRestricted( `Found ${packageManager} version ${packageManagerVersion} in ${packageManagerToolFolder}` ); - InstallHelpers._ensureLocalPackageManagerSymlink( + await InstallHelpers._ensureLocalPackageManagerSymlinkAsync( rushConfiguration, packageManager, packageManagerToolFolder, @@ -485,7 +485,7 @@ export class InstallHelpers { await packageManagerMarker.createAsync(); - InstallHelpers._ensureLocalPackageManagerSymlink( + await InstallHelpers._ensureLocalPackageManagerSymlinkAsync( rushConfiguration, packageManager, packageManagerToolFolder, @@ -496,12 +496,12 @@ export class InstallHelpers { } } - private static _ensureLocalPackageManagerSymlink( + private static async _ensureLocalPackageManagerSymlinkAsync( rushConfiguration: RushConfiguration, packageManager: PackageManagerName, packageManagerToolFolder: string, logIfConsoleOutputIsNotRestricted: (message?: string) => void - ): void { + ): Promise { // Example: "C:\MyRepo\common\temp" FileSystem.ensureFolder(rushConfiguration.commonTempFolder); From 37fea3f0a76cbb060576e597e433eac3a75fc096 Mon Sep 17 00:00:00 2001 From: "chenzhelong.sirius" Date: Mon, 20 Jul 2026 19:43:11 +0800 Subject: [PATCH 6/6] test: isolate install helper mocks --- libraries/rush-lib/src/logic/test/InstallHelpers.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 2368803371..23d6188691 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -35,6 +35,10 @@ describe(InstallHelpers.name, () => { mockJsonFileSaveAsync.mockClear(); }); + afterAll(() => { + jest.restoreAllMocks(); + }); + it('generates correct package json with pnpm configurations', async () => { const RUSH_JSON_FILENAME: string = `${__dirname}/pnpmConfig/rush.json`; const rushConfiguration: RushConfiguration = @@ -177,6 +181,7 @@ describe(InstallHelpers.name, () => { } beforeEach(() => { + jest.restoreAllMocks(); FileSystem.ensureEmptyFolder(tempFolderPath); });