From 24c6954e9c20791f3ee76f44974378e03b5b0ee7 Mon Sep 17 00:00:00 2001 From: David Michon Date: Wed, 20 Aug 2025 02:24:02 +0000 Subject: [PATCH 1/4] [rush] Optimize setPreferredVersions --- ...setPreferredVersions_2025-08-20-02-23.json | 10 +++ .../rush-lib/src/logic/pnpm/PnpmfileShim.ts | 69 ++++++++++++++++--- 2 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 common/changes/@microsoft/rush/tune-setPreferredVersions_2025-08-20-02-23.json diff --git a/common/changes/@microsoft/rush/tune-setPreferredVersions_2025-08-20-02-23.json b/common/changes/@microsoft/rush/tune-setPreferredVersions_2025-08-20-02-23.json new file mode 100644 index 00000000000..ea6d90efbc3 --- /dev/null +++ b/common/changes/@microsoft/rush/tune-setPreferredVersions_2025-08-20-02-23.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Optimize `setPreferredVersions` in install setup.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts index e906f9bb008..628a4e0c09c 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts @@ -16,15 +16,21 @@ import type { IPnpmfile, IPnpmfileShimSettings, IPnpmfileContext, IPnpmfileHooks let settings: IPnpmfileShimSettings | undefined; let allPreferredVersions: Map | undefined; +let rangeCache: Map | undefined; let allowedAlternativeVersions: Map> | undefined; +let workspaceVersions: Map | undefined; let userPnpmfile: IPnpmfile | undefined; let semver: typeof TSemver | undefined; +const SEMVER_COMPARE_OPTIONS: TSemver.RangeOptions = { includePrerelease: true }; + // Resets the internal state of the pnpmfile export function reset(): void { settings = undefined; allPreferredVersions = undefined; allowedAlternativeVersions = undefined; + workspaceVersions = undefined; + rangeCache = undefined; userPnpmfile = undefined; semver = undefined; } @@ -56,6 +62,7 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext { } if (!allPreferredVersions && settings.allPreferredVersions) { allPreferredVersions = new Map(Object.entries(settings.allPreferredVersions)); + rangeCache = new Map(); } if (!allowedAlternativeVersions && settings.allowedAlternativeVersions) { allowedAlternativeVersions = new Map( @@ -64,6 +71,9 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext { }) ); } + if (!workspaceVersions && settings.workspaceVersions) { + workspaceVersions = new Map(Object.entries(settings.workspaceVersions)); + } // If a userPnpmfilePath is provided, we expect it to exist if (!userPnpmfile && settings.userPnpmfilePath) { userPnpmfile = require(settings.userPnpmfilePath); @@ -76,27 +86,61 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext { return context as IPnpmfileContext; } +function parseRange(range: string): TSemver.Range | false { + if (!rangeCache || !semver) { + return false; + } + + if (range.includes(':')) { + return false; + } + + const entry: TSemver.Range | false | undefined = rangeCache.get(range); + if (entry !== undefined) { + return entry; + } + + try { + const parsedRange: TSemver.Range = new semver.Range(range, SEMVER_COMPARE_OPTIONS); + rangeCache.set(range, parsedRange); + return parsedRange; + } catch { + rangeCache.set(range, false); + return false; + } +} + // Set the preferred versions on the dependency map. If the version on the map is an allowedAlternativeVersion // then skip it. Otherwise, check to ensure that the common version is a subset of the specified version. If // it is, then replace the specified version with the preferredVersion function setPreferredVersions(dependencies: { [dependencyName: string]: string } | undefined): void { - for (const [name, version] of Object.entries(dependencies || {})) { + if (!dependencies || !semver) { + return; + } + + for (const [name, version] of Object.entries(dependencies)) { + if (version?.includes(':')) { + // Skip any specifier that isn't a raw SemVer range. + continue; + } + const preferredVersion: string | undefined = allPreferredVersions?.get(name); + // If preferredVersionRange is valid and the current version is not an allowed alternative, proceed to check subsets if (preferredVersion && !allowedAlternativeVersions?.get(name)?.has(version)) { - let preferredVersionRange: TSemver.Range | undefined; + const preferredVersionRange: TSemver.Range | false = parseRange(preferredVersion); + if (!preferredVersionRange) { + continue; + } + let versionRange: TSemver.Range | undefined; try { - preferredVersionRange = new semver!.Range(preferredVersion); - versionRange = new semver!.Range(version); + versionRange = new semver.Range(version, SEMVER_COMPARE_OPTIONS); } catch { // Swallow invalid range errors } - if ( - preferredVersionRange && - versionRange && - semver!.subset(preferredVersionRange, versionRange, { includePrerelease: true }) - ) { - dependencies![name] = preferredVersion; + + if (versionRange && semver.subset(preferredVersionRange, versionRange, SEMVER_COMPARE_OPTIONS)) { + dependencies[name] = preferredVersion; } } } @@ -115,7 +159,10 @@ export const hooks: IPnpmfileHooks = { readPackage: (pkg: IPackageJson, context: IPnpmfileContext) => { context = init(context); setPreferredVersions(pkg.dependencies); - setPreferredVersions(pkg.devDependencies); + if (workspaceVersions && workspaceVersions.get(pkg.name) === pkg.version) { + // devDependencies are only installed for workspace packages, so the rest of the time we can save the trouble of scanning. + setPreferredVersions(pkg.devDependencies); + } setPreferredVersions(pkg.optionalDependencies); return userPnpmfile?.hooks?.readPackage ? userPnpmfile.hooks.readPackage(pkg, context) : pkg; }, From ce986d957a299b500ac2cae42ca9fa07af99f89b Mon Sep 17 00:00:00 2001 From: David Michon Date: Thu, 21 Aug 2025 23:40:55 +0000 Subject: [PATCH 2/4] Revise shim --- .../rush-lib/src/logic/pnpm/PnpmfileShim.ts | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts index 628a4e0c09c..b96649cf56b 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts @@ -16,12 +16,14 @@ import type { IPnpmfile, IPnpmfileShimSettings, IPnpmfileContext, IPnpmfileHooks let settings: IPnpmfileShimSettings | undefined; let allPreferredVersions: Map | undefined; -let rangeCache: Map | undefined; +let rangeParseCache: Map | undefined; let allowedAlternativeVersions: Map> | undefined; let workspaceVersions: Map | undefined; let userPnpmfile: IPnpmfile | undefined; let semver: typeof TSemver | undefined; +// All calls to new semver.Range and the comparison functions need to have the same options +// It can be a different object with the same properties, but reusing this const avoids allocations const SEMVER_COMPARE_OPTIONS: TSemver.RangeOptions = { includePrerelease: true }; // Resets the internal state of the pnpmfile @@ -30,7 +32,7 @@ export function reset(): void { allPreferredVersions = undefined; allowedAlternativeVersions = undefined; workspaceVersions = undefined; - rangeCache = undefined; + rangeParseCache = undefined; userPnpmfile = undefined; semver = undefined; } @@ -62,7 +64,7 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext { } if (!allPreferredVersions && settings.allPreferredVersions) { allPreferredVersions = new Map(Object.entries(settings.allPreferredVersions)); - rangeCache = new Map(); + rangeParseCache = new Map(); } if (!allowedAlternativeVersions && settings.allowedAlternativeVersions) { allowedAlternativeVersions = new Map( @@ -87,25 +89,26 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext { } function parseRange(range: string): TSemver.Range | false { - if (!rangeCache || !semver) { + if (!rangeParseCache || !semver) { return false; } - if (range.includes(':')) { - return false; - } - - const entry: TSemver.Range | false | undefined = rangeCache.get(range); + const entry: TSemver.Range | false | undefined = rangeParseCache.get(range); if (entry !== undefined) { return entry; } + if (range.includes(':')) { + rangeParseCache.set(range, false); + return false; + } + try { const parsedRange: TSemver.Range = new semver.Range(range, SEMVER_COMPARE_OPTIONS); - rangeCache.set(range, parsedRange); + rangeParseCache.set(range, parsedRange); return parsedRange; } catch { - rangeCache.set(range, false); + rangeParseCache.set(range, false); return false; } } @@ -119,11 +122,6 @@ function setPreferredVersions(dependencies: { [dependencyName: string]: string } } for (const [name, version] of Object.entries(dependencies)) { - if (version?.includes(':')) { - // Skip any specifier that isn't a raw SemVer range. - continue; - } - const preferredVersion: string | undefined = allPreferredVersions?.get(name); // If preferredVersionRange is valid and the current version is not an allowed alternative, proceed to check subsets if (preferredVersion && !allowedAlternativeVersions?.get(name)?.has(version)) { @@ -132,14 +130,12 @@ function setPreferredVersions(dependencies: { [dependencyName: string]: string } continue; } - let versionRange: TSemver.Range | undefined; - try { - versionRange = new semver.Range(version, SEMVER_COMPARE_OPTIONS); - } catch { - // Swallow invalid range errors + const versionRange: TSemver.Range | false = parseRange(version); + if (!versionRange) { + continue; } - if (versionRange && semver.subset(preferredVersionRange, versionRange, SEMVER_COMPARE_OPTIONS)) { + if (semver.subset(preferredVersionRange, versionRange, SEMVER_COMPARE_OPTIONS)) { dependencies[name] = preferredVersion; } } @@ -150,21 +146,24 @@ export const hooks: IPnpmfileHooks = { // Call the original pnpmfile (if it exists) afterAllResolved: (lockfile: IPnpmShrinkwrapYaml, context: IPnpmfileContext) => { context = init(context); - return userPnpmfile?.hooks?.afterAllResolved - ? userPnpmfile.hooks.afterAllResolved(lockfile, context) - : lockfile; + return userPnpmfile?.hooks?.afterAllResolved?.(lockfile, context) ?? lockfile; }, // Set the preferred versions in the package, then call the original pnpmfile (if it exists) readPackage: (pkg: IPackageJson, context: IPnpmfileContext) => { context = init(context); + // Apply the user pnpmfile readPackage hook first, in case it moves dependencies around, and so that it sees the true package.json + pkg = userPnpmfile?.hooks?.readPackage?.(pkg, context) ?? pkg; + + // Then do version refinement of preferredVersions, since this is just supposed to act as if pnpm "prefers" these resolutions during + // calculation. setPreferredVersions(pkg.dependencies); if (workspaceVersions && workspaceVersions.get(pkg.name) === pkg.version) { // devDependencies are only installed for workspace packages, so the rest of the time we can save the trouble of scanning. setPreferredVersions(pkg.devDependencies); } setPreferredVersions(pkg.optionalDependencies); - return userPnpmfile?.hooks?.readPackage ? userPnpmfile.hooks.readPackage(pkg, context) : pkg; + return pkg; }, // Call the original pnpmfile (if it exists) From ad7ad1f875763088c8d197effa12c27fe0d4c229 Mon Sep 17 00:00:00 2001 From: David Michon Date: Fri, 22 Aug 2025 00:05:23 +0000 Subject: [PATCH 3/4] Tune --- .../rush-lib/src/logic/pnpm/PnpmfileShim.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts index b96649cf56b..b9124078558 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts @@ -117,29 +117,33 @@ function parseRange(range: string): TSemver.Range | false { // then skip it. Otherwise, check to ensure that the common version is a subset of the specified version. If // it is, then replace the specified version with the preferredVersion function setPreferredVersions(dependencies: { [dependencyName: string]: string } | undefined): void { - if (!dependencies || !semver) { + if (!dependencies || !semver || !allPreferredVersions) { return; } - for (const [name, version] of Object.entries(dependencies)) { - const preferredVersion: string | undefined = allPreferredVersions?.get(name); + // Needed for control flow analyzer. + const definitelyDefinedAllPreferredVersions: Map = allPreferredVersions; + const definiteSemver: typeof TSemver = semver; + + Object.entries(dependencies).forEach(([name, version]: [string, string]) => { + const preferredVersion: string | undefined = definitelyDefinedAllPreferredVersions.get(name); // If preferredVersionRange is valid and the current version is not an allowed alternative, proceed to check subsets if (preferredVersion && !allowedAlternativeVersions?.get(name)?.has(version)) { const preferredVersionRange: TSemver.Range | false = parseRange(preferredVersion); if (!preferredVersionRange) { - continue; + return; } const versionRange: TSemver.Range | false = parseRange(version); if (!versionRange) { - continue; + return; } - if (semver.subset(preferredVersionRange, versionRange, SEMVER_COMPARE_OPTIONS)) { + if (definiteSemver.subset(preferredVersionRange, versionRange, SEMVER_COMPARE_OPTIONS)) { dependencies[name] = preferredVersion; } } - } + }); } export const hooks: IPnpmfileHooks = { From 8a7f6b26b0fa986ccb7b5a683cc5cd0383058424 Mon Sep 17 00:00:00 2001 From: David Michon Date: Fri, 22 Aug 2025 00:14:55 +0000 Subject: [PATCH 4/4] Add comment --- libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts index b9124078558..dee28859b8c 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmfileShim.ts @@ -99,6 +99,7 @@ function parseRange(range: string): TSemver.Range | false { } if (range.includes(':')) { + // This version specifier has a protocol (e.g. npm:, workspace:, etc.), so it is not a normal semver range. rangeParseCache.set(range, false); return false; }