@@ -58,13 +58,31 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
5858 // Collect all package keys using arborist
5959 collectDependencies ( targetNode , relevantPackageLocations ) ;
6060
61+
62+ // Build a map of package paths to their versions for collision detection
63+ function buildVersionMap ( relevantPackageLocations ) {
64+ const versionMap = new Map ( ) ;
65+ for ( const [ key , node ] of relevantPackageLocations ) {
66+ const pkgPath = key . split ( "|" ) [ 0 ] ;
67+ versionMap . set ( pkgPath , node . version ) ;
68+ }
69+ return versionMap ;
70+ }
71+
72+ // Build a map: key = package path, value = array of unique versions
73+ const existingLocationsAndVersions = buildVersionMap ( relevantPackageLocations ) ;
6174 // Using the keys, extract relevant package-entries from package-lock.json
75+ // Extract and process packages
6276 const extractedPackages = Object . create ( null ) ;
63- for ( let [ packageLoc , node ] of relevantPackageLocations ) {
64- let pkg = packageLockJson . packages [ packageLoc ] ;
77+ for ( const [ locAndParentPackage , node ] of relevantPackageLocations ) {
78+ const [ originalLocation , parentPackage ] = locAndParentPackage . split ( "|" ) ;
79+
80+ let pkg = packageLockJson . packages [ node . location ] ;
6581 if ( pkg . link ) {
6682 pkg = packageLockJson . packages [ pkg . resolved ] ;
6783 }
84+
85+ let packageLoc ;
6886 if ( pkg . name === targetPackageName ) {
6987 // Make the target package the root package
7088 packageLoc = "" ;
@@ -73,7 +91,13 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
7391 }
7492 } else {
7593 packageLoc = normalizePackageLocation (
76- packageLoc , node , targetPackageName , tree . packageName , relevantPackageLocations ) ;
94+ [ originalLocation , parentPackage ] ,
95+ node ,
96+ targetPackageName ,
97+ tree . packageName ,
98+ existingLocationsAndVersions ,
99+ targetNode
100+ ) ;
77101 }
78102 if ( packageLoc !== "" && ! pkg . resolved ) {
79103 // For all but the root package, ensure that "resolved" and "integrity" fields are present
@@ -106,61 +130,72 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
106130 return shrinkwrap ;
107131}
108132
109- /**
110- * Normalize package locations from workspace-specific paths to standard npm paths.
111- * Automatically detects and resolves version collisions by checking source locations.
112- *
113- * Examples (assuming @ui5/cli is the targetPackageName):
114- * - packages/cli/node_modules/foo -> node_modules/foo (if no collision)
115- * - packages/cli/node_modules/foo -> node_modules/@ui5/cli/node_modules/foo (if root has different version)
116- * - packages/fs/node_modules/bar -> node_modules/@ui5/fs/node_modules/bar
117- *
118- * @param {string } location - Package location from arborist
119- * @param {object } node - Package node from arborist
120- * @param {string } targetPackageName - Target package name for shrinkwrap file
121- * @param {string } rootPackageName - Root / workspace package name
122- * @param {Map<string, object> } relevantPackageLocations
123- * @returns {string } - Normalized location for npm-shrinkwrap.json
124- */
133+ function isDirectDependency ( node , targetNode ) {
134+ return Array . from ( node . edgesIn . values ( ) ) . some ( ( edge ) => edge . from === targetNode ) ;
135+ }
136+
125137function normalizePackageLocation (
126- location , node , targetPackageName , rootPackageName , relevantPackageLocations ) {
138+ [ location , parentPackage ] ,
139+ node ,
140+ targetPackageName ,
141+ rootPackageName ,
142+ existingLocationsAndVersions ,
143+ targetNode
144+ ) {
127145 const topPackageName = node . top . packageName ;
128146
129147 if ( topPackageName === targetPackageName ) {
130- // Package is within target package (e.g. @ui5/cli)
148+ // Package belongs to target package
131149 const normalizedPath = location . substring ( node . top . location . length + 1 ) ;
132- const existing = relevantPackageLocations . get ( normalizedPath ) ;
150+ const existingVersion = existingLocationsAndVersions . get ( normalizedPath ) ;
151+
152+ // Handle version collision
153+ if ( existingVersion && existingVersion !== node . version ) {
154+ // Direct dependencies get priority, transitive ones get nested
155+ const finalPath = isDirectDependency ( node , targetNode ) ?
156+ normalizedPath : `node_modules/${ topPackageName } /${ normalizedPath } ` ;
133157
134- // Check for version collision
135- if ( existing && existing . version !== node . version ) {
136- // Different version exists - nest this one under the target package
137- return `node_modules/${ topPackageName } /${ normalizedPath } ` ;
158+ existingLocationsAndVersions . set ( finalPath , node . version ) ;
159+ return finalPath ;
138160 }
139161
162+ existingLocationsAndVersions . set ( normalizedPath , node . version ) ;
140163 return normalizedPath ;
141164 } else if ( topPackageName !== rootPackageName ) {
142- // Add package within node_modules of actual package name (e.g. @ui5/fs)
143- return `node_modules/${ topPackageName } /${ location . substring ( node . top . location . length + 1 ) } ` ;
165+ // Package belongs to another workspace package - nest under it
166+ const nestedPath = `node_modules/${ topPackageName } /${ location . substring ( node . top . location . length + 1 ) } ` ;
167+ existingLocationsAndVersions . set ( nestedPath , node . version ) ;
168+ return nestedPath ;
169+ } else {
170+ const existingVersion = existingLocationsAndVersions . get ( location ) ;
171+ if ( existingVersion && existingVersion !== node . version ) {
172+ // Version collision at root - nest under parent
173+ const nestedPath = `node_modules/${ parentPackage } /${ location } ` ;
174+ existingLocationsAndVersions . set ( nestedPath , node . version ) ;
175+ return nestedPath ;
176+ }
144177 }
145178
146- // Package is within root workspace, keep as-is
147179 return location ;
148180}
149181
150- function collectDependencies ( node , relevantPackageLocations ) {
151- if ( relevantPackageLocations . has ( node . location ) ) {
182+ function collectDependencies ( node , relevantPackageLocations , parentPackage = "" ) {
183+ if ( relevantPackageLocations . has ( node . location + "|" + parentPackage ) ) {
152184 // Already processed
153185 return ;
154186 }
155- relevantPackageLocations . set ( node . location , node ) ;
187+ // We need this as the module could be in the root node_modules and later might need to be nested
188+ // under many different parent packages due to version collisions. If this step is skipped, some
189+ // packages might be missing in the final shrinkwrap due to key overwrites.
190+ relevantPackageLocations . set ( node . location + "|" + parentPackage , node ) ;
156191 if ( node . isLink ) {
157192 node = node . target ;
158193 }
159194 for ( const edge of node . edgesOut . values ( ) ) {
160195 if ( edge . dev ) {
161196 continue ;
162197 }
163- collectDependencies ( edge . to , relevantPackageLocations ) ;
198+ collectDependencies ( edge . to , relevantPackageLocations , node . packageName ) ;
164199 }
165200}
166201
0 commit comments