@@ -48,73 +48,26 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
4848 path : workspaceRootDir ,
4949 } ) ;
5050 const tree = await arb . loadVirtual ( ) ;
51- const targetNode = tree . inventory . get ( `node_modules/${ targetPackageName } ` ) ;
51+ let targetNode = tree . inventory . get ( `node_modules/${ targetPackageName } ` ) ;
5252 if ( ! targetNode ) {
5353 throw new Error ( `Target package "${ targetPackageName } " not found in workspace` ) ;
5454 }
55+ targetNode = targetNode . isLink ? targetNode . target : targetNode ;
5556
56- const relevantPackageLocations = new Map ( ) ;
57+ const virtualFlatTree = [ ] ;
5758 // Collect all package keys using arborist
58- collectDependencies ( targetNode , relevantPackageLocations ) ;
59-
59+ resolveVirtualTree ( targetNode , virtualFlatTree ) ;
6060
61+ const physicalTree = new Map ( ) ;
62+ await buildPhysicalTree (
63+ virtualFlatTree , physicalTree , packageLockJson , workspaceRootDir ) ;
6164 // Build a map of package paths to their versions for collision detection
62- function buildVersionMap ( relevantPackageLocations ) {
63- const versionMap = new Map ( ) ;
64- for ( const [ key , node ] of relevantPackageLocations ) {
65- const pkgPath = key . split ( "|" ) [ 0 ] ;
66- versionMap . set ( pkgPath , node . version ) ;
67- }
68- return versionMap ;
69- }
70-
71- // Build a map: key = package path, value = array of unique versions
72- const existingLocationsAndVersions = buildVersionMap ( relevantPackageLocations ) ;
73- // Using the keys, extract relevant package-entries from package-lock.json
74- // Extract and process packages
75- const extractedPackages = Object . create ( null ) ;
76- for ( const [ locAndParentPackage , node ] of relevantPackageLocations ) {
77- const [ originalLocation , parentPackage ] = locAndParentPackage . split ( "|" ) ;
78-
79- let pkg = packageLockJson . packages [ node . location ] ;
80- if ( pkg . link ) {
81- pkg = packageLockJson . packages [ pkg . resolved ] ;
82- }
83-
84- let packageLoc ;
85- if ( pkg . name === targetPackageName ) {
86- // Make the target package the root package
87- packageLoc = "" ;
88- if ( extractedPackages [ packageLoc ] ) {
89- throw new Error ( `Duplicate root package entry for "${ targetPackageName } "` ) ;
90- }
91- } else {
92- packageLoc = normalizePackageLocation (
93- [ originalLocation , parentPackage ] ,
94- node ,
95- targetPackageName ,
96- tree . packageName ,
97- existingLocationsAndVersions ,
98- targetNode
99- ) ;
100- }
101- if ( packageLoc !== "" && ! pkg . resolved ) {
102- // For all but the root package, ensure that "resolved" and "integrity" fields are present
103- // These are always missing for locally linked packages, but sometimes also for others (e.g. if installed
104- // from local cache)
105- const { resolved, integrity} = await fetchPackageMetadata ( node . packageName , node . version , workspaceRootDir ) ;
106- pkg . resolved = resolved ;
107- pkg . integrity = integrity ;
108- }
109-
110- extractedPackages [ packageLoc ] = pkg ;
111- }
11265
11366 // Sort packages by key to ensure consistent order (just like the npm cli does it)
11467 const sortedExtractedPackages = Object . create ( null ) ;
115- const sortedKeys = Object . keys ( extractedPackages ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
68+ const sortedKeys = Array . from ( physicalTree . keys ( ) ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
11669 for ( const key of sortedKeys ) {
117- sortedExtractedPackages [ key ] = extractedPackages [ key ] ;
70+ sortedExtractedPackages [ key ] = physicalTree . get ( key ) [ 0 ] ;
11871 }
11972
12073 // Generate npm-shrinkwrap.json
@@ -129,73 +82,107 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
12982 return shrinkwrap ;
13083}
13184
132- function isDirectDependency ( node , targetNode ) {
133- return Array . from ( node . edgesIn . values ( ) ) . some ( ( edge ) => edge . from === targetNode ) ;
134- }
135-
136- function normalizePackageLocation (
137- [ location , parentPackage ] ,
138- node ,
139- targetPackageName ,
140- rootPackageName ,
141- existingLocationsAndVersions ,
142- targetNode
143- ) {
144- const topPackageName = node . top . packageName ;
145-
146- if ( topPackageName === targetPackageName ) {
147- // Package belongs to target package
148- const normalizedPath = location . substring ( node . top . location . length + 1 ) ;
149- const existingVersion = existingLocationsAndVersions . get ( normalizedPath ) ;
150-
151- // Handle version collision
152- if ( existingVersion && existingVersion !== node . version ) {
153- // Direct dependencies get priority, transitive ones get nested
154- const finalPath = isDirectDependency ( node , targetNode ) ?
155- normalizedPath : `node_modules/${ topPackageName } /${ normalizedPath } ` ;
156-
157- existingLocationsAndVersions . set ( finalPath , node . version ) ;
158- return finalPath ;
159- }
160-
161- existingLocationsAndVersions . set ( normalizedPath , node . version ) ;
162- return normalizedPath ;
163- } else if ( topPackageName !== rootPackageName ) {
164- // Package belongs to another workspace package - nest under it
165- const nestedPath = `node_modules/${ topPackageName } /${ location . substring ( node . top . location . length + 1 ) } ` ;
166- existingLocationsAndVersions . set ( nestedPath , node . version ) ;
167- return nestedPath ;
168- } else {
169- const existingVersion = existingLocationsAndVersions . get ( location ) ;
170- if ( existingVersion && existingVersion !== node . version ) {
171- // Version collision at root - nest under parent
172- const nestedPath = `node_modules/${ parentPackage } /${ location } ` ;
173- existingLocationsAndVersions . set ( nestedPath , node . version ) ;
174- return nestedPath ;
175- }
176- }
85+ function resolveVirtualTree ( node , virtualFlatTree , curPath , parentNode ) {
86+ const fullPath = [ curPath , node . name ] . join ( " | " ) ;
17787
178- return location ;
179- }
180-
181- function collectDependencies ( node , relevantPackageLocations , parentPackage = "" ) {
182- if ( relevantPackageLocations . has ( node . location + "|" + parentPackage ) ) {
183- // Already processed
88+ if ( virtualFlatTree . some ( ( [ path ] ) => path === fullPath ) ) {
18489 return ;
18590 }
186- // We need this as the module could be in the root node_modules and later might need to be nested
187- // under many different parent packages due to version collisions. If this step is skipped, some
188- // packages might be missing in the final shrinkwrap due to key overwrites.
189- relevantPackageLocations . set ( node . location + "|" + parentPackage , node ) ;
19091 if ( node . isLink ) {
19192 node = node . target ;
19293 }
94+
95+ virtualFlatTree . push ( [ fullPath , [ node , parentNode ] ] ) ;
96+
19397 for ( const edge of node . edgesOut . values ( ) ) {
19498 if ( edge . dev ) {
19599 continue ;
196100 }
197- collectDependencies ( edge . to , relevantPackageLocations , node . packageName ) ;
101+ resolveVirtualTree ( edge . to , virtualFlatTree , fullPath , node ) ;
102+ }
103+ }
104+
105+ async function buildPhysicalTree (
106+ virtualFlatTree , physicalTree , packageLockJson , workspaceRootDir ) {
107+ // Sort by path depth and then alphabetically to ensure parent
108+ // packages are processed before children. It's important to
109+ // process parents first to correctly handle version collisions and hoisting
110+ virtualFlatTree . sort ( ( [ pathA ] , [ pathB ] ) => {
111+ if ( pathA . split ( " | " ) . length < pathB . split ( " | " ) . length ) {
112+ return - 1 ;
113+ } else if ( pathA . split ( " | " ) . length > pathB . split ( " | " ) . length ) {
114+ return 1 ;
115+ } else {
116+ return pathA . localeCompare ( pathB ) ;
117+ }
118+ } ) ;
119+ const targetNode = virtualFlatTree [ 0 ] [ 1 ] [ 0 ] ;
120+ const targetPackageName = targetNode . packageName ;
121+
122+ for ( const [ , nodes ] of virtualFlatTree ) {
123+ let packageLoc ;
124+ let [ node , parentNode ] = nodes ;
125+ const { location, version} = node ;
126+ const pkg = packageLockJson . packages [ location ] ;
127+
128+ if ( node . isLink ) {
129+ // For linked packages, use the target node
130+ node = node . target ;
131+ }
132+
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+ }
145+
146+
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 } "` ) ;
157+ }
158+
159+ if ( packageLoc !== "" && ! pkg . resolved ) {
160+ // For all but the root package, ensure that "resolved" and "integrity" fields are present
161+ // These are always missing for locally linked packages, but sometimes also for others (e.g. if installed
162+ // from local cache)
163+ const { resolved, integrity} =
164+ await fetchPackageMetadata ( node . packageName , node . version , workspaceRootDir ) ;
165+ pkg . resolved = resolved ;
166+ pkg . integrity = integrity ;
167+ }
168+
169+ physicalTree . set ( packageLoc , [ pkg , node ] ) ;
170+ }
171+ }
172+
173+ function normalizePackageLocation ( location , node , targetPackageName ) {
174+ const topPackageName = node . top . packageName ;
175+ const rootPackageName = node . root . packageName ;
176+ let curLocation = location ;
177+ if ( topPackageName === targetPackageName ) {
178+ // Remove location for packages within target package (e.g. @ui5/cli)
179+ curLocation = location . substring ( node . top . location . length + 1 ) ;
180+ } else if ( topPackageName !== rootPackageName ) {
181+ // Add package within node_modules of actual package name (e.g. @ui5/fs)
182+ curLocation = `node_modules/${ topPackageName } /${ location . substring ( node . top . location . length + 1 ) } ` ;
198183 }
184+ // If it's already within the root workspace package, keep as-is
185+ return curLocation . endsWith ( "/" ) ? curLocation . slice ( 0 , - 1 ) : curLocation ;
199186}
200187
201188/**
0 commit comments