Skip to content

Commit 40d48f6

Browse files
committed
refactor: Properly handle shrinkwrap root dependencies
1 parent 7149a46 commit 40d48f6

File tree

1 file changed

+68
-33
lines changed

1 file changed

+68
-33
lines changed

internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
125137
function 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

Comments
 (0)