Skip to content

Commit 1da08f6

Browse files
committed
refactor: Use virtual and physical trees to resolve shrinkwrap
1 parent c0d5f11 commit 1da08f6

1 file changed

Lines changed: 85 additions & 28 deletions

File tree

internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,16 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t
8383
}
8484

8585
function 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+
173230
function normalizePackageLocation(location, node, targetPackageName) {
174231
const topPackageName = node.top.packageName;
175232
const rootPackageName = node.root.packageName;

0 commit comments

Comments
 (0)