@@ -83,11 +83,16 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
8383}
8484
8585function resolveVirtualTree ( node , virtualFlatTree , curPath , parentNode ) {
86+ if ( node . isLink ) {
87+ node = node . target ;
88+ }
89+
8690 const fullPath = [ curPath , node . name ] . join ( " | " ) ;
8791
8892 if ( virtualFlatTree . some ( ( [ path ] ) => path === fullPath ) ) {
8993 return ;
9094 }
95+
9196 if ( node . isLink ) {
9297 node = node . target ;
9398 }
@@ -98,6 +103,7 @@ function resolveVirtualTree(node, virtualFlatTree, curPath, parentNode) {
98103 if ( edge . dev ) {
99104 continue ;
100105 }
106+
101107 resolveVirtualTree ( edge . to , virtualFlatTree , fullPath , node ) ;
102108 }
103109}
@@ -119,41 +125,70 @@ async function buildPhysicalTree(
119125 const targetNode = virtualFlatTree [ 0 ] [ 1 ] [ 0 ] ;
120126 const targetPackageName = targetNode . packageName ;
121127
128+ // Collect information to resolve potential version conflicts later
129+ const statsToResolveConflicts = new Map ( ) ;
130+ for ( const [ , nodes ] of virtualFlatTree ) {
131+ const packageLoc = resolveLocation ( nodes , physicalTree , targetPackageName ) ;
132+ const [ node , parentNode ] = nodes ;
133+ const { version} = node ;
134+ const isTargetPackageHardDep = ( parentNode ?. packageName === targetPackageName ) ;
135+
136+ // index 0: Set of versions found for this location
137+ // index 1: Map of version -> count
138+ // (this will be used eventually to elect the most common version in root node_modules)
139+ // index 2: If target package has direct dependency here, the version
140+ const packageStats = statsToResolveConflicts . get ( packageLoc ) || [ new Set ( ) , Object . create ( null ) ] ;
141+ packageStats [ 0 ] . add ( version ) ;
142+ packageStats [ 1 ] [ version ] ??= 0 ;
143+ packageStats [ 1 ] [ version ] ++ ;
144+ if ( isTargetPackageHardDep ) {
145+ if ( packageStats [ 2 ] ) {
146+ throw new Error ( `Impossible to resolve hoisting conflicts. ` +
147+ `Target package direct dependency "${ node . packageName } " ` +
148+ `has multiple versions: ${ packageStats [ 2 ] } and ${ version } .` ) ;
149+ }
150+ packageStats [ 2 ] = version ;
151+ }
152+
153+ statsToResolveConflicts . set ( packageLoc , packageStats ) ;
154+ }
155+
156+ const resolvedPackageLocations = new Map ( ) ;
122157 for ( const [ , nodes ] of virtualFlatTree ) {
123- let packageLoc ;
124- let [ node , parentNode ] = nodes ;
158+ let packageLoc = resolveLocation ( nodes , physicalTree , targetPackageName ) ;
159+ const [ node , parentNode ] = nodes ;
125160 const { location, version} = node ;
126161 const pkg = packageLockJson . packages [ location ] ;
127162
128- if ( node . isLink ) {
129- // For linked packages, use the target node
130- node = node . target ;
131- }
163+ const isRootNodeModulesLocation = `node_modules/${ node . packageName } ` === packageLoc ;
164+ const isTargetModuleDependency = ( parentNode ?. packageName === targetPackageName ) ;
132165
133- if ( node . packageName === targetPackageName ) {
134- // Make the target package the root package
135- packageLoc = "" ;
136- if ( physicalTree [ location ] ) {
137- throw new Error ( `Duplicate root package entry for "${ targetPackageName } "` ) ;
138- }
139- } else if ( node . parent ?. packageName === targetPackageName ) {
140- // Direct dependencies of the target package go into node_modules.
141- packageLoc = `node_modules/${ node . packageName } ` ;
142- } else {
143- packageLoc = normalizePackageLocation ( location , node , targetPackageName ) ;
144- }
166+ // Handle version conflicts in root node_modules
167+ if ( isRootNodeModulesLocation && ! isTargetModuleDependency ) {
168+ const packageStats = statsToResolveConflicts . get ( packageLoc ) ;
169+ const hasConflictingLocationAndVersion = packageStats [ 0 ] . size > 1 ;
170+ // Which is the version of the package that's (eventually) used as
171+ // dependency of the target package.
172+ let selectedVersionForRootNodeModules = version ;
145173
174+ if ( hasConflictingLocationAndVersion ) {
175+ const targetPackageVersion = packageStats [ 2 ] ;
176+ const versionsCount = packageStats [ 1 ] ;
177+ // Use target package direct dependency version if available,
178+ // otherwise elect the most common version among dependents
179+ selectedVersionForRootNodeModules = targetPackageVersion ??
180+ Object . keys ( packageStats [ 1 ] ) . reduce ( ( acc , versionKey ) => {
181+ return versionsCount [ acc ] > versionsCount [ versionKey ] ? acc : versionKey ;
182+ } ) ;
183+ }
146184
147- const [ , existingNode ] = physicalTree . get ( packageLoc ) ?? [ ] ;
148- // TODO: Optimize this
149- const pathAlreadyReserved = virtualFlatTree
150- . find ( ( record ) => record [ 1 ] [ 0 ] . location === packageLoc ) ?. [ 1 ] ?. [ 0 ] ;
151- if ( ( existingNode && existingNode . version !== version ) ||
152- ( pathAlreadyReserved && pathAlreadyReserved . version !== version )
153- ) {
154- const parentPath = normalizePackageLocation ( parentNode . location , parentNode , targetPackageName ) ;
155- packageLoc = parentPath ? `${ parentPath } /${ packageLoc } ` : packageLoc ;
156- console . warn ( `Duplicate package location detected: "${ packageLoc } "` ) ;
185+ if ( selectedVersionForRootNodeModules !== version ) {
186+ const parentPath = resolvedPackageLocations . get ( parentNode ) ??
187+ // Fallback in case parentNode is not yet resolved (should never happen)
188+ // check virtualFlatTree.sort(...) above
189+ normalizePackageLocation ( parentNode . location , parentNode , targetPackageName ) ;
190+ packageLoc = parentPath ? `${ parentPath } /${ packageLoc } ` : packageLoc ;
191+ }
157192 }
158193
159194 if ( packageLoc !== "" && ! pkg . resolved ) {
@@ -166,10 +201,32 @@ async function buildPhysicalTree(
166201 pkg . integrity = integrity ;
167202 }
168203
204+ resolvedPackageLocations . set ( node , packageLoc ) ;
169205 physicalTree . set ( packageLoc , [ pkg , node ] ) ;
170206 }
171207}
172208
209+ function resolveLocation ( nodes , physicalTree , targetPackageName ) {
210+ let packageLoc ;
211+ const [ node , parentNode ] = nodes ;
212+ const { location} = node ;
213+
214+ if ( node . packageName === targetPackageName ) {
215+ // Make the target package the root package
216+ packageLoc = "" ;
217+ if ( physicalTree [ location ] ) {
218+ throw new Error ( `Duplicate root package entry for "${ targetPackageName } "` ) ;
219+ }
220+ } else if ( parentNode ?. packageName === targetPackageName ) {
221+ // Direct dependencies of the target package go into node_modules.
222+ packageLoc = `node_modules/${ node . packageName } ` ;
223+ } else {
224+ packageLoc = normalizePackageLocation ( location , node , targetPackageName ) ;
225+ }
226+
227+ return packageLoc ;
228+ }
229+
173230function normalizePackageLocation ( location , node , targetPackageName ) {
174231 const topPackageName = node . top . packageName ;
175232 const rootPackageName = node . root . packageName ;
0 commit comments