tools/@aws-cdk/yarn-cling/lib/hoisting.ts (96 lines of code) (raw):

import { PackageLockPackage } from './types'; /** * Hoist package-lock dependencies in-place * * This happens in two phases: * * 1) Move every package into the parent scope (as long as it introduces no conflicts). * This step mutates the dependency tree. * 2) Once no more packages can be moved up, clean up the tree. This step mutates the * tree declarations but cannot change versions of required packages. Two cleanups: * 2a) Remove duplicates down the tree (same version that is inherited from above) * 2b) Remove useless packages that aren't depended upon by anything in that subtree. * To determine whether a package is useful or useless in a tree, we record * each package's original dependencies before we start messing around in the * tree. * * This two-phase process replaces a proces that did move-and-delete as one step, which * sometimes would hoist a package into a place that was previously vacated by a conflicting * version, thereby causing the wrong version to be loaded. * * Hoisting is still rather expensive on a large tree (~100ms), we should find ways to * speed it up. */ export function hoistDependencies(packageTree: PackageLockPackage) { const originalDependencies = new Map<PackageLockPackage, string[]>(); recordOriginalDependencies(packageTree); moveUp(packageTree); removeDupes(packageTree, []); removeUseless(packageTree); // Move the children of the parent onto the same level if there are no conflicts function moveUp(node: PackageLockPackage, parent?: PackageLockPackage) { if (!node.dependencies) { return; } // Recurse for (const child of Object.values(node.dependencies)) { moveUp(child, node); } // Then push packages from the current node into its parent if (parent) { for (const [depName, depPackage] of Object.entries(node.dependencies)) { if (!parent.dependencies?.[depName]) { // It's new and there's no version conflict, we can move it up. parent.dependencies![depName] = depPackage; } } } } function removeDupes(node: PackageLockPackage, rootPath: Array<PackageLockPackage>) { if (!node.dependencies) { return; } // Any dependencies here that are the same in the parent can be removed for (const [depName, depPackage] of Object.entries(node.dependencies)) { if (findInheritedDepVersion(depName, rootPath) === depPackage.version) { delete node.dependencies[depName]; } } // Recurse for (const child of Object.values(node.dependencies)) { removeDupes(child, [node, ...rootPath]); } } function removeUseless(node: PackageLockPackage) { if (!node.dependencies) { return; } for (const [depName, depPkg] of Object.entries(node.dependencies)) { if (!necessaryInTree(depName, depPkg.version, node)) { delete node.dependencies[depName]; } } // Recurse for (const child of Object.values(node.dependencies)) { removeUseless(child); } // If we ended up with empty dependencies, just get rid of the key (for clean printing) if (Object.keys(node.dependencies).length === 0) { delete node.dependencies; } } function findInheritedDepVersion(name: string, parentDependenciesChain: Array<PackageLockPackage>) { for (const deps of parentDependenciesChain) { if (deps.dependencies?.[name]) { return deps.dependencies[name].version; } } return undefined; } function recordOriginalDependencies(node: PackageLockPackage) { if (!node.dependencies) { return; } let list = originalDependencies.get(node); if (!list) { list = []; originalDependencies.set(node, list); } for (const [depName, depPkg] of Object.entries(node.dependencies)) { list.push(`${depName}@${depPkg.version}`); recordOriginalDependencies(depPkg); } } function necessaryInTree(name: string, version: string, tree: PackageLockPackage) { if (originalDependencies.get(tree)?.includes(`${name}@${version}`)) { return true; } if (!tree.dependencies) { return false; } for (const depPackage of Object.values(tree.dependencies)) { if (necessaryInTree(name, version, depPackage)) { return true; } } return false; } } export function renderTree(tree: PackageLockPackage): string[] { const ret = new Array<string>(); recurse(tree.dependencies ?? {}, []); return ret.sort(compareSplit); function recurse(n: Record<string, PackageLockPackage>, parts: string[]) { for (const [k, v] of Object.entries(n)) { ret.push([...parts, k].join('.') + '=' + v.version); recurse(v.dependencies ?? {}, [...parts, k]); } } function compareSplit(a: string, b: string): number { // Sort so that: 'a=1', 'a.b=2' get sorted in that order. const as = a.split(/\.|=/g); const bs = b.split(/\.|=/g); for (let i = 0; i < as.length && i < bs.length; i++) { const cmp = as[i].localeCompare(bs[i]); if (cmp !== 0) { return cmp; } } return as.length - bs.length; } }