@@ -16,15 +16,23 @@ import type { IPnpmfile, IPnpmfileShimSettings, IPnpmfileContext, IPnpmfileHooks
1616
1717let settings : IPnpmfileShimSettings | undefined ;
1818let allPreferredVersions : Map < string , string > | undefined ;
19+ let rangeParseCache : Map < string , TSemver . Range | false > | undefined ;
1920let allowedAlternativeVersions : Map < string , Set < string > > | undefined ;
21+ let workspaceVersions : Map < string , string > | undefined ;
2022let userPnpmfile : IPnpmfile | undefined ;
2123let semver : typeof TSemver | undefined ;
2224
25+ // All calls to new semver.Range and the comparison functions need to have the same options
26+ // It can be a different object with the same properties, but reusing this const avoids allocations
27+ const SEMVER_COMPARE_OPTIONS : TSemver . RangeOptions = { includePrerelease : true } ;
28+
2329// Resets the internal state of the pnpmfile
2430export function reset ( ) : void {
2531 settings = undefined ;
2632 allPreferredVersions = undefined ;
2733 allowedAlternativeVersions = undefined ;
34+ workspaceVersions = undefined ;
35+ rangeParseCache = undefined ;
2836 userPnpmfile = undefined ;
2937 semver = undefined ;
3038}
@@ -56,6 +64,7 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext {
5664 }
5765 if ( ! allPreferredVersions && settings . allPreferredVersions ) {
5866 allPreferredVersions = new Map ( Object . entries ( settings . allPreferredVersions ) ) ;
67+ rangeParseCache = new Map ( ) ;
5968 }
6069 if ( ! allowedAlternativeVersions && settings . allowedAlternativeVersions ) {
6170 allowedAlternativeVersions = new Map (
@@ -64,6 +73,9 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext {
6473 } )
6574 ) ;
6675 }
76+ if ( ! workspaceVersions && settings . workspaceVersions ) {
77+ workspaceVersions = new Map ( Object . entries ( settings . workspaceVersions ) ) ;
78+ }
6779 // If a userPnpmfilePath is provided, we expect it to exist
6880 if ( ! userPnpmfile && settings . userPnpmfilePath ) {
6981 userPnpmfile = require ( settings . userPnpmfilePath ) ;
@@ -76,48 +88,87 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext {
7688 return context as IPnpmfileContext ;
7789}
7890
91+ function parseRange ( range : string ) : TSemver . Range | false {
92+ if ( ! rangeParseCache || ! semver ) {
93+ return false ;
94+ }
95+
96+ const entry : TSemver . Range | false | undefined = rangeParseCache . get ( range ) ;
97+ if ( entry !== undefined ) {
98+ return entry ;
99+ }
100+
101+ if ( range . includes ( ':' ) ) {
102+ // This version specifier has a protocol (e.g. npm:, workspace:, etc.), so it is not a normal semver range.
103+ rangeParseCache . set ( range , false ) ;
104+ return false ;
105+ }
106+
107+ try {
108+ const parsedRange : TSemver . Range = new semver . Range ( range , SEMVER_COMPARE_OPTIONS ) ;
109+ rangeParseCache . set ( range , parsedRange ) ;
110+ return parsedRange ;
111+ } catch {
112+ rangeParseCache . set ( range , false ) ;
113+ return false ;
114+ }
115+ }
116+
79117// Set the preferred versions on the dependency map. If the version on the map is an allowedAlternativeVersion
80118// then skip it. Otherwise, check to ensure that the common version is a subset of the specified version. If
81119// it is, then replace the specified version with the preferredVersion
82120function setPreferredVersions ( dependencies : { [ dependencyName : string ] : string } | undefined ) : void {
83- for ( const [ name , version ] of Object . entries ( dependencies || { } ) ) {
84- const preferredVersion : string | undefined = allPreferredVersions ?. get ( name ) ;
121+ if ( ! dependencies || ! semver || ! allPreferredVersions ) {
122+ return ;
123+ }
124+
125+ // Needed for control flow analyzer.
126+ const definitelyDefinedAllPreferredVersions : Map < string , string > = allPreferredVersions ;
127+ const definiteSemver : typeof TSemver = semver ;
128+
129+ Object . entries ( dependencies ) . forEach ( ( [ name , version ] : [ string , string ] ) => {
130+ const preferredVersion : string | undefined = definitelyDefinedAllPreferredVersions . get ( name ) ;
131+ // If preferredVersionRange is valid and the current version is not an allowed alternative, proceed to check subsets
85132 if ( preferredVersion && ! allowedAlternativeVersions ?. get ( name ) ?. has ( version ) ) {
86- let preferredVersionRange : TSemver . Range | undefined ;
87- let versionRange : TSemver . Range | undefined ;
88- try {
89- preferredVersionRange = new semver ! . Range ( preferredVersion ) ;
90- versionRange = new semver ! . Range ( version ) ;
91- } catch {
92- // Swallow invalid range errors
133+ const preferredVersionRange : TSemver . Range | false = parseRange ( preferredVersion ) ;
134+ if ( ! preferredVersionRange ) {
135+ return ;
136+ }
137+
138+ const versionRange : TSemver . Range | false = parseRange ( version ) ;
139+ if ( ! versionRange ) {
140+ return ;
93141 }
94- if (
95- preferredVersionRange &&
96- versionRange &&
97- semver ! . subset ( preferredVersionRange , versionRange , { includePrerelease : true } )
98- ) {
99- dependencies ! [ name ] = preferredVersion ;
142+
143+ if ( definiteSemver . subset ( preferredVersionRange , versionRange , SEMVER_COMPARE_OPTIONS ) ) {
144+ dependencies [ name ] = preferredVersion ;
100145 }
101146 }
102- }
147+ } ) ;
103148}
104149
105150export const hooks : IPnpmfileHooks = {
106151 // Call the original pnpmfile (if it exists)
107152 afterAllResolved : ( lockfile : IPnpmShrinkwrapYaml , context : IPnpmfileContext ) => {
108153 context = init ( context ) ;
109- return userPnpmfile ?. hooks ?. afterAllResolved
110- ? userPnpmfile . hooks . afterAllResolved ( lockfile , context )
111- : lockfile ;
154+ return userPnpmfile ?. hooks ?. afterAllResolved ?.( lockfile , context ) ?? lockfile ;
112155 } ,
113156
114157 // Set the preferred versions in the package, then call the original pnpmfile (if it exists)
115158 readPackage : ( pkg : IPackageJson , context : IPnpmfileContext ) => {
116159 context = init ( context ) ;
160+ // Apply the user pnpmfile readPackage hook first, in case it moves dependencies around, and so that it sees the true package.json
161+ pkg = userPnpmfile ?. hooks ?. readPackage ?.( pkg , context ) ?? pkg ;
162+
163+ // Then do version refinement of preferredVersions, since this is just supposed to act as if pnpm "prefers" these resolutions during
164+ // calculation.
117165 setPreferredVersions ( pkg . dependencies ) ;
118- setPreferredVersions ( pkg . devDependencies ) ;
166+ if ( workspaceVersions && workspaceVersions . get ( pkg . name ) === pkg . version ) {
167+ // devDependencies are only installed for workspace packages, so the rest of the time we can save the trouble of scanning.
168+ setPreferredVersions ( pkg . devDependencies ) ;
169+ }
119170 setPreferredVersions ( pkg . optionalDependencies ) ;
120- return userPnpmfile ?. hooks ?. readPackage ? userPnpmfile . hooks . readPackage ( pkg , context ) : pkg ;
171+ return pkg ;
121172 } ,
122173
123174 // Call the original pnpmfile (if it exists)
0 commit comments