diff --git a/.changeset/pretty-toys-agree.md b/.changeset/pretty-toys-agree.md new file mode 100644 index 0000000000..f0b5dbe2e6 --- /dev/null +++ b/.changeset/pretty-toys-agree.md @@ -0,0 +1,5 @@ +--- +"@pnpm/npm-resolver": patch +--- + +Improve performance of clean install by preconverting and caching semver objects diff --git a/resolving/npm-resolver/src/pickPackageFromMeta.ts b/resolving/npm-resolver/src/pickPackageFromMeta.ts index 6d90e35350..d0ba57a65e 100644 --- a/resolving/npm-resolver/src/pickPackageFromMeta.ts +++ b/resolving/npm-resolver/src/pickPackageFromMeta.ts @@ -58,6 +58,33 @@ export function pickPackageFromMeta ( } } +const semverRangeCache = new Map() + +// This is a performance optimization; working with string-ish semver +// causes lots of allocations and repeated work, but caching the Range +// and ensuring we give it a SemVer instance greatly speeds things up. +function semverSatisfiesLoose (version: string, range: string): boolean { + let semverRange = semverRangeCache.get(range) + if (semverRange === undefined) { + try { + semverRange = new semver.Range(range, true) + } catch { + semverRange = null + } + semverRangeCache.set(range, semverRange) + } + + if (semverRange) { + try { + return semverRange.test(new semver.SemVer(version, true)) + } catch { + return false + } + } + + return false +} + export function pickLowestVersionByVersionRange ( meta: PackageMeta, versionRange: string, @@ -89,7 +116,7 @@ export function pickVersionByVersionRange ( if (preferredVerSels != null && Object.keys(preferredVerSels).length > 0) { const prioritizedPreferredVersions = prioritizePreferredVersions(meta, versionRange, preferredVerSels) for (const preferredVersions of prioritizedPreferredVersions) { - if (preferredVersions.includes(latest) && semver.satisfies(latest, versionRange, true)) { + if (preferredVersions.includes(latest) && semverSatisfiesLoose(latest, versionRange)) { return latest } const preferredVersion = semver.maxSatisfying(preferredVersions, versionRange, true) @@ -106,7 +133,7 @@ export function pickVersionByVersionRange ( latest = undefined } } - if (latest && (versionRange === '*' || semver.satisfies(latest, versionRange, true))) { + if (latest && (versionRange === '*' || semverSatisfiesLoose(latest, versionRange))) { // Not using semver.satisfies in case of * because it does not select beta versions. // E.g.: 1.0.0-beta.1. See issue: https://github.com/pnpm/pnpm/issues/865 return latest @@ -144,12 +171,9 @@ function prioritizePreferredVersions ( break } case 'range': { - // This might be slow if there are many versions - // and the package is an indirect dependency many times in the project. - // If it will create noticeable slowdown, then might be a good idea to add some caching const versions = Object.keys(meta.versions) for (const version of versions) { - if (semver.satisfies(version, preferredSelector, true)) { + if (semverSatisfiesLoose(version, preferredSelector)) { versionsPrioritizer.add(version, weight) } }