Skip to content

Commit 071340f

Browse files
committed
refactor: Start rewriting logic in 2 steps
Get the virtual tree in a flat format Sort it, so that it the loop runs top to bottom Start rewriting modules to the target root
1 parent f25562d commit 071340f

File tree

1 file changed

+99
-112
lines changed

1 file changed

+99
-112
lines changed

internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js

Lines changed: 99 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)