@@ -65,14 +65,17 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
6565 if ( pkg . link ) {
6666 pkg = packageLockJson . packages [ pkg . resolved ] ;
6767 }
68+
69+ const originalLocation = packageLoc ;
6870 if ( pkg . name === targetPackageName ) {
6971 // Make the target package the root package
7072 packageLoc = "" ;
7173 if ( extractedPackages [ packageLoc ] ) {
7274 throw new Error ( `Duplicate root package entry for "${ targetPackageName } "` ) ;
7375 }
7476 } else {
75- packageLoc = normalizePackageLocation ( packageLoc , node , targetPackageName , tree . packageName ) ;
77+ packageLoc = normalizePackageLocation ( packageLoc , node , targetPackageName , tree . packageName ,
78+ extractedPackages ) ;
7679 }
7780 if ( packageLoc !== "" && ! pkg . resolved ) {
7881 // For all but the root package, ensure that "resolved" and "integrity" fields are present
@@ -82,14 +85,15 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
8285 pkg . resolved = resolved ;
8386 pkg . integrity = integrity ;
8487 }
85- extractedPackages [ packageLoc ] = pkg ;
88+
89+ extractedPackages [ packageLoc ] = { pkg, originalLocation} ;
8690 }
8791
8892 // Sort packages by key to ensure consistent order (just like the npm cli does it)
8993 const sortedExtractedPackages = Object . create ( null ) ;
9094 const sortedKeys = Object . keys ( extractedPackages ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
9195 for ( const key of sortedKeys ) {
92- sortedExtractedPackages [ key ] = extractedPackages [ key ] ;
96+ sortedExtractedPackages [ key ] = extractedPackages [ key ] . pkg ;
9397 }
9498
9599 // Generate npm-shrinkwrap.json
@@ -106,26 +110,57 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
106110
107111/**
108112 * Normalize package locations from workspace-specific paths to standard npm paths.
113+ * Automatically detects and resolves version collisions by checking source locations.
114+ *
109115 * Examples (assuming @ui5/cli is the targetPackageName):
110- * - packages/cli/node_modules/foo -> node_modules/foo
116+ * - packages/cli/node_modules/foo -> node_modules/foo (if no collision)
117+ * - packages/cli/node_modules/foo -> node_modules/@ui5/cli/node_modules/foo (if root has different version)
111118 * - packages/fs/node_modules/bar -> node_modules/@ui5/fs/node_modules/bar
112119 *
113120 * @param {string } location - Package location from arborist
114121 * @param {object } node - Package node from arborist
115122 * @param {string } targetPackageName - Target package name for shrinkwrap file
116123 * @param {string } rootPackageName - Root / workspace package name
124+ * @param {Object<string, {pkg: object, originalLocation: string}> } extractedPackages - Already extracted packages
117125 * @returns {string } - Normalized location for npm-shrinkwrap.json
118126 */
119- function normalizePackageLocation ( location , node , targetPackageName , rootPackageName ) {
127+ function normalizePackageLocation (
128+ location , node , targetPackageName , rootPackageName , extractedPackages
129+ ) {
120130 const topPackageName = node . top . packageName ;
131+ const currentIsFromRoot = ! location . startsWith ( "packages/" ) ;
132+
121133 if ( topPackageName === targetPackageName ) {
122- // Remove location for packages within target package (e.g. @ui5/cli)
123- return location . substring ( node . top . location . length + 1 ) ;
134+ // Package is within target package (e.g. @ui5/cli)
135+ const normalizedPath = location . substring ( node . top . location . length + 1 ) ;
136+ const existing = extractedPackages [ normalizedPath ] ;
137+
138+ if ( existing && existing . pkg . version !== node . version ) {
139+ // Collision detected: Check which should be at root level
140+ const existingIsFromRoot = ! existing . originalLocation . startsWith ( "packages/" ) ;
141+
142+ // Root packages always get priority at top level
143+ if ( existingIsFromRoot && ! currentIsFromRoot ) {
144+ // Existing is from root, current is from workspace -> nest current
145+ return `node_modules/${ topPackageName } /${ normalizedPath } ` ;
146+ } else if ( ! existingIsFromRoot && currentIsFromRoot ) {
147+ // Current is from root, existing is from workspace -> shouldn't happen
148+ // due to processing order, but handle gracefully
149+ const msg = `Unexpected collision: root package processed after workspace ` +
150+ `package at ${ normalizedPath } ` ;
151+ throw new Error ( msg ) ;
152+ }
153+ // Both from same type of location with different versions -> nest current
154+ return `node_modules/${ topPackageName } /${ normalizedPath } ` ;
155+ }
156+
157+ return normalizedPath ;
124158 } else if ( topPackageName !== rootPackageName ) {
125159 // Add package within node_modules of actual package name (e.g. @ui5/fs)
126160 return `node_modules/${ topPackageName } /${ location . substring ( node . top . location . length + 1 ) } ` ;
127161 }
128- // If it's already within the root workspace package, keep as-is
162+
163+ // Package is within root workspace, keep as-is
129164 return location ;
130165}
131166
0 commit comments