@yarnpkg/pnpify
Advanced tools
Comparing version 2.0.0-rc.18 to 2.0.0-rc.19
@@ -7,2 +7,12 @@ import { PortablePath, Filename } from '@yarnpkg/fslib'; | ||
} | ||
export declare type NodeModulesBaseNode = { | ||
dirList: Set<Filename>; | ||
}; | ||
export declare type NodeModulesPackageNode = { | ||
locator: LocatorKey; | ||
target: PortablePath; | ||
linkType: LinkType; | ||
dirList?: undefined; | ||
aliases: string[]; | ||
}; | ||
/** | ||
@@ -17,11 +27,3 @@ * Node modules tree - a map of every folder within the node_modules, along with their | ||
*/ | ||
export declare type NodeModulesTree = Map<PortablePath, { | ||
dirList: Set<Filename>; | ||
} | { | ||
dirList?: undefined; | ||
locator: LocatorKey; | ||
target: PortablePath; | ||
linkType: LinkType; | ||
aliases: string[]; | ||
}>; | ||
export declare type NodeModulesTree = Map<PortablePath, NodeModulesBaseNode | NodeModulesPackageNode>; | ||
export interface NodeModulesTreeOptions { | ||
@@ -28,0 +30,0 @@ pnpifyFs?: boolean; |
@@ -36,3 +36,3 @@ "use strict"; | ||
const packageTree = buildPackageTree(pnp); | ||
const hoistedTree = hoist_1.hoist(packageTree, { check: false }); | ||
const hoistedTree = hoist_1.hoist(packageTree); | ||
return populateNodeModulesTree(pnp, hoistedTree, options); | ||
@@ -53,2 +53,10 @@ }; | ||
} | ||
for (const val of map.values()) { | ||
// Sort locations by depth first and then alphabetically for determinism | ||
val.locations = val.locations.sort((loc1, loc2) => { | ||
const len1 = loc1.split(fslib_1.ppath.delimiter).length; | ||
const len2 = loc2.split(fslib_1.ppath.delimiter).length; | ||
return len1 !== len2 ? len2 - len1 : loc2.localeCompare(loc1); | ||
}); | ||
} | ||
return map; | ||
@@ -66,3 +74,7 @@ }; | ||
const topPkg = pnp.getPackageInformation(pnp.topLevel); | ||
if (topPkg === null) | ||
throw new Error(`Assertion failed: Expected the top-level package to have been registered`); | ||
const topLocator = pnp.findPackageLocator(topPkg.packageLocation); | ||
if (topLocator === null) | ||
throw new Error(`Assertion failed: Expected the top-level package to have a physical locator`); | ||
const topLocatorKey = stringifyLocator(topLocator); | ||
@@ -81,15 +93,22 @@ for (const locator of pnpRoots) { | ||
const nodes = new Map(); | ||
const addPackageToTree = (pkg, locator, parent) => { | ||
const addPackageToTree = (pkg, locator, parent, parentPkg) => { | ||
const locatorKey = stringifyLocator(locator); | ||
let node = nodes.get(locatorKey); | ||
const isSeen = !!node; | ||
if (locator === topLocator) | ||
if (!isSeen && locatorKey === topLocatorKey) { | ||
node = packageTree; | ||
nodes.set(locatorKey, packageTree); | ||
} | ||
if (!node) { | ||
const { name, reference } = locator; | ||
// TODO: remove this code when `packagePeers` will not contain regular dependencies | ||
const peerNames = new Set(); | ||
for (const peerName of pkg.packagePeers) | ||
if (pkg.packageDependencies.get(peerName) === parentPkg.packageDependencies.get(peerName)) | ||
peerNames.add(peerName); | ||
node = { | ||
name: name, | ||
reference: reference, | ||
name, | ||
reference, | ||
dependencies: new Set(), | ||
peerNames: pkg.packagePeers, | ||
peerNames, | ||
}; | ||
@@ -101,10 +120,12 @@ nodes.set(locatorKey, node); | ||
for (const [name, referencish] of pkg.packageDependencies) { | ||
if (referencish !== null) { | ||
if (referencish !== null && !node.peerNames.has(name)) { | ||
const depLocator = pnp.getLocator(name, referencish); | ||
const pkgLocator = pnp.getLocator(name.replace('$wsroot$', ''), referencish); | ||
const depPkg = pnp.getPackageInformation(pkgLocator); | ||
if (depPkg === null) | ||
throw new Error(`Assertion failed: Expected the package to have been registered`); | ||
// Skip package self-references | ||
if (stringifyLocator(depLocator) !== locatorKey) { | ||
addPackageToTree(depPkg, depLocator, node); | ||
} | ||
if (stringifyLocator(depLocator) === locatorKey) | ||
continue; | ||
addPackageToTree(depPkg, depLocator, node, pkg); | ||
} | ||
@@ -114,3 +135,3 @@ } | ||
}; | ||
addPackageToTree(topPkg, topLocator, packageTree); | ||
addPackageToTree(topPkg, topLocator, packageTree, topPkg); | ||
return packageTree; | ||
@@ -133,2 +154,4 @@ }; | ||
const info = pnp.getPackageInformation(pkgLocator); | ||
if (info === null) | ||
throw new Error(`Assertion failed: Expected the package to be registered`); | ||
let linkType; | ||
@@ -146,3 +169,5 @@ let target; | ||
else { | ||
const truePath = pnp.resolveVirtual && locator.reference && locator.reference.startsWith('virtual:') ? pnp.resolveVirtual(info.packageLocation) : info.packageLocation; | ||
const truePath = pnp.resolveVirtual && locator.reference && locator.reference.startsWith('virtual:') | ||
? pnp.resolveVirtual(info.packageLocation) | ||
: info.packageLocation; | ||
target = fslib_1.npath.toPortablePath(truePath || info.packageLocation); | ||
@@ -160,3 +185,9 @@ linkType = info.linkType; | ||
const [nameOrScope, name] = locator.name.split('/'); | ||
return name ? { scope: fslib_1.toFilename(nameOrScope), name: fslib_1.toFilename(name) } : { scope: null, name: fslib_1.toFilename(nameOrScope) }; | ||
return name ? { | ||
scope: fslib_1.toFilename(nameOrScope), | ||
name: fslib_1.toFilename(name), | ||
} : { | ||
scope: null, | ||
name: fslib_1.toFilename(nameOrScope), | ||
}; | ||
}; | ||
@@ -172,3 +203,5 @@ const seenNodes = new Set(); | ||
const { name, scope } = getPackageName(locator); | ||
const packageNameParts = scope ? [scope, name] : [name]; | ||
const packageNameParts = scope | ||
? [scope, name] | ||
: [name]; | ||
const nodeModulesDirPath = fslib_1.ppath.join(locationPrefix, NODE_MODULES); | ||
@@ -175,0 +208,0 @@ const nodeModulesLocation = fslib_1.ppath.join(nodeModulesDirPath, ...packageNameParts); |
@@ -34,7 +34,11 @@ #!/usr/bin/env node | ||
let currProjectRoot = null; | ||
let isCJS = ''; | ||
while (nextProjectRoot !== currProjectRoot) { | ||
currProjectRoot = nextProjectRoot; | ||
nextProjectRoot = fslib_1.ppath.dirname(currProjectRoot); | ||
if (fslib_1.xfs.existsSync(fslib_1.ppath.join(currProjectRoot, `.pnp.js`))) { | ||
if (fslib_1.xfs.existsSync(fslib_1.ppath.join(currProjectRoot, `.pnp.js`))) | ||
break; | ||
if (fslib_1.xfs.existsSync(fslib_1.ppath.join(currProjectRoot, `.pnp.cjs`))) { | ||
isCJS = 'c'; | ||
break; | ||
} | ||
@@ -44,3 +48,3 @@ } | ||
throw new Error(`This tool can only be used with projects using Yarn Plug'n'Play`); | ||
const pnpPath = fslib_1.ppath.join(currProjectRoot, `.pnp.js`); | ||
const pnpPath = fslib_1.ppath.join(currProjectRoot, `.pnp.${isCJS}js`); | ||
const pnpApi = dynamicRequire_1.dynamicRequire(pnpPath); | ||
@@ -47,0 +51,0 @@ generateSdk_1.generateSdk(pnpApi).catch(error => { |
@@ -69,3 +69,3 @@ "use strict"; | ||
throw new Error(`Assertion failed: Package ${this.name} isn't a dependency of the top-level`); | ||
const manifest = dynamicRequire_1.dynamicRequire(`${this.name}/package.json`); | ||
const manifest = dynamicRequire_1.dynamicRequire(fslib_1.npath.join(pkgInformation.packageLocation, `package.json`)); | ||
await fslib_1.xfs.mkdirpPromise(fslib_1.ppath.dirname(absWrapperPath)); | ||
@@ -76,2 +76,3 @@ await fslib_1.xfs.writeFilePromise(absWrapperPath, JSON.stringify({ | ||
main: manifest.main, | ||
type: `commonjs`, | ||
}, null, 2)); | ||
@@ -109,2 +110,3 @@ } | ||
await wrapper.writeFile(`lib/tsserver.js`); | ||
await wrapper.writeFile(`lib/typescript.js`); | ||
await addVSCodeWorkspaceSettings(pnpApi, { | ||
@@ -111,0 +113,0 @@ [`typescript.tsdk`]: fslib_1.npath.fromPortablePath(fslib_1.ppath.dirname(wrapper.getProjectPathTo(`lib/tsserver.js`))), |
@@ -14,3 +14,4 @@ declare type PackageName = string; | ||
declare type HoistOptions = { | ||
check: boolean; | ||
check?: boolean; | ||
debugLevel?: number; | ||
}; | ||
@@ -27,3 +28,3 @@ /** | ||
*/ | ||
export declare const hoist: (tree: HoisterTree, options?: HoistOptions) => HoisterResult; | ||
export declare const hoist: (tree: HoisterTree, opts?: HoistOptions) => HoisterResult; | ||
export {}; |
511
lib/hoist.js
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const makeLocator = (name, reference) => `${name}@${reference}`; | ||
const makePhysicalLocator = (name, reference) => { | ||
const makeIdent = (name, reference) => { | ||
const hashIdx = reference.indexOf('#'); | ||
@@ -20,47 +20,49 @@ // Strip virtual reference part, we don't need it for hoisting purposes | ||
*/ | ||
exports.hoist = (tree, options = { check: false }) => { | ||
exports.hoist = (tree, opts = {}) => { | ||
const debugLevel = opts.debugLevel || Number(process.env.NM_DEBUG_LEVEL || -1); | ||
const check = opts.check || debugLevel >= 9; | ||
const options = { check, debugLevel }; | ||
if (options.debugLevel >= 0) | ||
console.time('hoist'); | ||
const treeCopy = cloneTree(tree); | ||
const ancestorMap = buildAncestorMap(treeCopy); | ||
hoistTo(treeCopy, treeCopy, ancestorMap, options); | ||
hoistTo(treeCopy, treeCopy, new Set([treeCopy.locator]), new Map(), ancestorMap, options); | ||
if (options.debugLevel >= 0) | ||
console.timeEnd('hoist'); | ||
if (options.debugLevel >= 3) { | ||
const identList = Array.from(ancestorMap.keys()); | ||
identList.sort((key1, key2) => ancestorMap.get(key2).size - ancestorMap.get(key1).size); | ||
console.log('Package popularity:'); | ||
for (const ident of identList) { | ||
console.log(ident, '→', ancestorMap.get(ident).size); | ||
} | ||
} | ||
if (options.debugLevel >= 1) { | ||
const checkLog = selfCheck(treeCopy); | ||
if (checkLog) { | ||
throw new Error(`${checkLog}, after hoisting finished:\n${dumpDepTree(treeCopy)}`); | ||
} | ||
} | ||
if (options.debugLevel >= 2) | ||
console.log(dumpDepTree(treeCopy)); | ||
return shrinkTree(treeCopy); | ||
}; | ||
const selfCheck = (tree) => { | ||
let log = []; | ||
const getHoistedDependencies = (rootNode) => { | ||
const hoistedDependencies = new Map(); | ||
const seenNodes = new Set(); | ||
const checkNode = (node, parentDeps, parents) => { | ||
const addHoistedDependencies = (node) => { | ||
if (seenNodes.has(node)) | ||
return; | ||
seenNodes.add(node); | ||
if (parents.has(node)) | ||
return; | ||
const dependencies = new Map(parentDeps); | ||
for (const dep of node.dependencies.values()) | ||
if (!node.peerNames.has(dep.name)) | ||
dependencies.set(dep.name, dep); | ||
for (const origDep of node.originalDependencies.values()) { | ||
const dep = dependencies.get(origDep.name); | ||
if (node.peerNames.has(origDep.name)) { | ||
const parentDep = parentDeps.get(origDep.name); | ||
if (parentDep !== dep) { | ||
log.push(`${Array.from(parents).concat([node]).map(x => x.locator).join('#')} - broken peer promise: expected ${dep.locator} but found ${parentDep ? parentDep.locator : parentDep}`); | ||
} | ||
} | ||
else { | ||
if (!dep) { | ||
log.push(`${Array.from(parents).concat([node]).map(x => x.locator).join('#')} - broken require promise: no required dependency ${origDep.locator} found`); | ||
} | ||
else if (dep.physicalLocator !== origDep.physicalLocator) { | ||
log.push(`${Array.from(parents).concat([node]).map(x => x.locator).join('#')} - broken require promise: expected ${origDep.physicalLocator}, but found: ${dep.physicalLocator}`); | ||
} | ||
} | ||
} | ||
const nextParents = new Set(parents).add(node); | ||
for (const dep of node.hoistedDependencies.values()) | ||
if (!rootNode.dependencies.has(dep.name)) | ||
hoistedDependencies.set(dep.name, dep); | ||
for (const dep of node.dependencies.values()) { | ||
if (!node.peerNames.has(dep.name)) { | ||
checkNode(dep, dependencies, nextParents); | ||
addHoistedDependencies(dep); | ||
} | ||
} | ||
}; | ||
checkNode(tree, tree.dependencies, new Set()); | ||
return log.join('\n'); | ||
addHoistedDependencies(rootNode); | ||
return hoistedDependencies; | ||
}; | ||
@@ -78,8 +80,8 @@ /** | ||
* The regular and peer dependency promises are kept while performing transform | ||
* on triples of packages at a time: | ||
* `root package` -> `parent package` -> `dependency` | ||
* on tree branches of packages at a time: | ||
* `root package` -> `parent package 1` ... `parent package n` -> `dependency` | ||
* We check wether we can hoist `dependency` to `root package`, this boils down basically | ||
* to checking: | ||
* 1. Wether `root package` does not depend on other version of `dependency` | ||
* 2. Wether all the peer dependencies of a `dependency` had already been hoisted from `parent package` | ||
* 2. Wether all the peer dependencies of a `dependency` had already been hoisted from all `parent packages` | ||
* | ||
@@ -89,181 +91,279 @@ * If many versions of the `dependency` can be hoisted to the `root package` we choose the most used | ||
* | ||
* This algorithm is shallow first, e.g. it transforms the tree: | ||
* . -> A -> B -> C | ||
* in this order: | ||
* 1) . -> A | ||
* -> B -> C | ||
* 2) . -> A | ||
* -> B | ||
* -> C | ||
* | ||
* This function mutates the tree. | ||
* | ||
* @param tree package dependencies graph | ||
* @param rootNode root node to hoist to | ||
* @param rootNodePath root node path in the tree | ||
* @param parentAncestorDependencies commulative dependencies of all root node ancestors, excluding root node dependenciew | ||
* @param ancestorMap ancestor map | ||
* @param options hoisting options | ||
*/ | ||
const hoistTo = (tree, rootNode, ancestorMap, options, seenNodes = new Set()) => { | ||
const hoistTo = (tree, rootNode, rootNodePath, parentAncestorDependencies, ancestorMap, options, seenNodes = new Set()) => { | ||
if (seenNodes.has(rootNode)) | ||
return 0; | ||
seenNodes.add(rootNode); | ||
// Perform shallow-first hoisting by hoisting to the root node first | ||
let totalHoisted = hoistPass(tree, rootNode, ancestorMap, options); | ||
let childrenHoisted = 0; | ||
// Now perform children hoisting | ||
const ancestorDependencies = new Map(parentAncestorDependencies); | ||
for (const dep of rootNode.dependencies.values()) | ||
childrenHoisted += hoistTo(tree, dep, ancestorMap, options, seenNodes); | ||
if (childrenHoisted > 0) | ||
// Perfrom 2nd pass of hoisting to the root node, because some of the children were hoisted | ||
hoistPass(tree, rootNode, ancestorMap, options); | ||
return totalHoisted + childrenHoisted; | ||
}; | ||
const hoistPass = (tree, rootNode, ancestorMap, options) => { | ||
let totalHoisted = 0; | ||
let packagesToHoist; | ||
const clonedParents = new Map(); | ||
if (!rootNode.peerNames.has(dep.name)) | ||
ancestorDependencies.set(dep.name, dep); | ||
const hoistedDependencies = rootNode === tree ? new Map() : getHoistedDependencies(rootNode); | ||
let clonedTree = { clone: rootNode, children: new Map() }; | ||
let hoistCandidates; | ||
do { | ||
packagesToHoist = getHoistablePackages(rootNode, ancestorMap); | ||
totalHoisted += packagesToHoist.size; | ||
for (const { parent, node } of packagesToHoist) { | ||
let parentNode = clonedParents.get(parent); | ||
if (!parentNode) { | ||
const { name, references, physicalLocator, locator, dependencies, originalDependencies, hoistedDependencies, peerNames } = parent; | ||
// To perform node hoisting from parent node we must clone parent node first, | ||
// because some other package in the tree might depend on the parent package where hoisting | ||
// cannot be performed | ||
parentNode = { | ||
name, | ||
references: new Set(references), | ||
physicalLocator, | ||
locator, | ||
dependencies: new Map(dependencies), | ||
originalDependencies: new Map(originalDependencies), | ||
hoistedDependencies: new Map(hoistedDependencies), | ||
peerNames: new Set(peerNames), | ||
}; | ||
clonedParents.set(parent, parentNode); | ||
rootNode.dependencies.set(parentNode.name, parentNode); | ||
} | ||
// Delete hoisted node from parent node | ||
parentNode.dependencies.delete(node.name); | ||
parentNode.hoistedDependencies.set(node.name, node); | ||
const hoistedNode = rootNode.dependencies.get(node.name); | ||
// Add hoisted node to root node, in case it is not already there | ||
if (!hoistedNode) { | ||
// Avoid adding node to itself | ||
if (node.physicalLocator !== rootNode.physicalLocator) { | ||
rootNode.dependencies.set(node.name, node); | ||
hoistCandidates = getHoistCandidates(rootNode, rootNodePath, ancestorDependencies, hoistedDependencies, ancestorMap, options); | ||
for (const hoistSet of hoistCandidates) { | ||
for (const { nodePath, node } of hoistSet.candidates) { | ||
let parentClonedNode = clonedTree; | ||
for (const originalNode of nodePath) { | ||
let nodeClone = parentClonedNode.children.get(originalNode); | ||
if (!nodeClone) { | ||
const { name, references, ident, locator, dependencies, originalDependencies, hoistedDependencies, peerNames, reasons } = originalNode; | ||
// To perform node hoisting from parent node we must clone parent nodes up to the root node, | ||
// because some other package in the tree might depend on the parent package where hoisting | ||
// cannot be performed | ||
const clone = { | ||
name, | ||
references: new Set(references), | ||
ident, | ||
locator, | ||
dependencies: new Map(dependencies), | ||
originalDependencies: new Map(originalDependencies), | ||
hoistedDependencies: new Map(hoistedDependencies), | ||
peerNames: new Set(peerNames), | ||
reasons: new Map(reasons), | ||
}; | ||
nodeClone = { clone, children: new Map() }; | ||
const selfDep = clone.dependencies.get(name); | ||
if (selfDep && selfDep.ident == clone.ident) | ||
// Update self-reference | ||
clone.dependencies.set(name, clone); | ||
parentClonedNode.children.set(originalNode, nodeClone); | ||
parentClonedNode.clone.dependencies.set(name, clone); | ||
} | ||
parentClonedNode = nodeClone; | ||
} | ||
} | ||
else { | ||
for (const reference of node.references) { | ||
hoistedNode.references.add(reference); | ||
parentClonedNode.clone.dependencies.delete(node.name); | ||
parentClonedNode.clone.hoistedDependencies.set(node.name, node); | ||
parentClonedNode.clone.reasons.delete(node.name); | ||
const hoistedNode = rootNode.dependencies.get(node.name); | ||
// Add hoisted node to root node, in case it is not already there | ||
if (!hoistedNode) { | ||
// Avoid adding other version of root node to itself | ||
if (rootNode.ident !== node.ident) { | ||
rootNode.dependencies.set(node.name, node); | ||
ancestorDependencies.set(node.name, node); | ||
} | ||
} | ||
} | ||
if (options.check) { | ||
const checkLog = selfCheck(tree); | ||
if (checkLog) { | ||
throw new Error(`After hoisting ${rootNode.locator}#${parent.physicalLocator}#${node.physicalLocator}:\n${require('util').inspect(node, { depth: null })}\n${checkLog}`); | ||
else { | ||
for (const reference of node.references) { | ||
hoistedNode.references.add(reference); | ||
} | ||
} | ||
if (options.check) { | ||
const checkLog = selfCheck(tree); | ||
if (checkLog) { | ||
throw new Error(`${checkLog}, after hoisting ${[rootNode, ...nodePath, node].map(x => prettyPrintLocator(x.locator)).join('→')}:\n${dumpDepTree(tree)}`); | ||
} | ||
} | ||
} | ||
} | ||
} while (packagesToHoist.size > 0); | ||
return totalHoisted; | ||
} while (hoistCandidates.size > 0); | ||
for (const dependency of rootNode.dependencies.values()) { | ||
if (!rootNode.peerNames.has(dependency.name) && !rootNodePath.has(dependency.locator)) { | ||
rootNodePath.add(dependency.locator); | ||
hoistTo(tree, dependency, rootNodePath, ancestorDependencies, ancestorMap, options); | ||
rootNodePath.delete(dependency.locator); | ||
} | ||
} | ||
}; | ||
/** | ||
* Finds all the packages that can be hoisted to the root package node from the set of: | ||
* `root node` -> `dependency` -> `subdependency` | ||
* `root node` -> ... `parent dependency j` ... -> `dependency` -> `subdependency` | ||
* | ||
* @param rootNode root package node | ||
* @param rootNodePath root node path in the tree | ||
* @param ancestorDependencies commulative dependencies of all root node ancestors, including root node dependencies | ||
* @param ancestorMap ancestor map to determine `dependency` version popularity | ||
*/ | ||
const getHoistablePackages = (rootNode, ancestorMap) => { | ||
const getHoistCandidates = (rootNode, rootNodePath, ancestorDependencies, hoistedDependencies, ancestorMap, options) => { | ||
const hoistCandidates = new Map(); | ||
const computeHoistCandidates = (parentNode, node) => { | ||
const parents = []; | ||
const seenNodes = new Set(); | ||
const computeHoistCandidates = (nodePath, locatorPath, node) => { | ||
const isSeen = seenNodes.has(node); | ||
let reasonRoot; | ||
let reason; | ||
if (options.debugLevel >= 2) | ||
reasonRoot = `${Array.from(rootNodePath).map(x => prettyPrintLocator(x)).join('→')}`; | ||
let isHoistable = true; | ||
let competitorInfo = hoistCandidates.get(node.name); | ||
const ancestorNode = ancestorMap.get(node.physicalLocator); | ||
const weight = ancestorNode.size; | ||
if (isHoistable) { | ||
const isCompatiblePhysicalLocator = (rootNode.name !== node.name || rootNode.physicalLocator === node.physicalLocator); | ||
isHoistable = isCompatiblePhysicalLocator; | ||
const isRegularDepAtRoot = !rootNode.peerNames.has(node.name); | ||
if (options.debugLevel >= 2 && !isRegularDepAtRoot) | ||
reason = `- is a peer dependency at ${reasonRoot}`; | ||
isHoistable = isRegularDepAtRoot; | ||
} | ||
let competitorInfo; | ||
let weight; | ||
let rootDep; | ||
if (isHoistable) { | ||
const rootDep = rootNode.dependencies.get(node.name); | ||
const origRootDep = rootNode.originalDependencies.get(node.name); | ||
const hoistedRootDep = rootNode.hoistedDependencies.get(node.name); | ||
const isNameAvailable = (!hoistedRootDep || hoistedRootDep.physicalLocator === node.physicalLocator) | ||
&& (!rootDep || rootDep.physicalLocator === node.physicalLocator) | ||
&& (!origRootDep || origRootDep.physicalLocator === node.physicalLocator); | ||
isHoistable = isNameAvailable; | ||
const isCompatibleIdent = (rootNode.name !== node.name || rootNode.ident === node.ident); | ||
if (options.debugLevel >= 2 && !isCompatibleIdent) | ||
reason = `- conflicts with ${reasonRoot}`; | ||
isHoistable = isCompatibleIdent; | ||
} | ||
if (isHoistable) { | ||
const isRegularDepAtRoot = !rootNode.peerNames.has(node.name); | ||
isHoistable = isRegularDepAtRoot; | ||
let isNameAvailable = false; | ||
const hoistedDep = hoistedDependencies.get(node.name); | ||
isNameAvailable = (!hoistedDep || hoistedDep.ident === node.ident); | ||
if (options.debugLevel >= 2 && !isNameAvailable) | ||
reason = `- filled by: ${prettyPrintLocator(hoistedDep.locator)} at ${reasonRoot}`; | ||
if (isNameAvailable) { | ||
for (const tuple of parents) { | ||
const parentDep = tuple.parent.dependencies.get(node.name); | ||
if (parentDep && parentDep.ident !== node.ident) { | ||
isNameAvailable = false; | ||
if (options.debugLevel >= 2) | ||
reason = `- filled by: ${prettyPrintLocator(parentDep.locator)} at ${prettyPrintLocator(tuple.parent.locator)}`; | ||
break; | ||
} | ||
} | ||
} | ||
isHoistable = isNameAvailable; | ||
} | ||
let isPreferred = false; | ||
if (isHoistable) { | ||
competitorInfo = hoistCandidates.get(node.name); | ||
weight = ancestorMap.get(node.ident).size; | ||
// If there is a competitor package to be hoisted, we should prefer the package with more usage | ||
isPreferred = !competitorInfo || competitorInfo.weight < weight; | ||
const isPreferred = !competitorInfo || competitorInfo.weight <= weight; | ||
if (options.debugLevel >= 2 && !isPreferred) | ||
reason = `- preferred package ${competitorInfo.node.locator} at ${reasonRoot}`; | ||
isHoistable = isPreferred; | ||
} | ||
if (isHoistable) { | ||
if (isHoistable && !rootDep) { | ||
let areRegularDepsSatisfied = true; | ||
// Check that hoisted dependencies of current node are satisifed | ||
for (const dep of node.hoistedDependencies.values()) { | ||
if (node.originalDependencies.has(dep.name)) { | ||
const rootDepNode = rootNode.dependencies.get(dep.name) || rootNode.hoistedDependencies.get(dep.name); | ||
if (!rootDepNode || rootDepNode.physicalLocator !== dep.physicalLocator) { | ||
isHoistable = false; | ||
const depNode = ancestorDependencies.get(dep.name); | ||
if (!depNode) { | ||
if (options.debugLevel >= 2) | ||
reason = `- hoisted dependency ${prettyPrintLocator(dep.locator)} is absent at ${reasonRoot}`; | ||
areRegularDepsSatisfied = false; | ||
} | ||
else if (depNode.ident !== dep.ident) { | ||
if (options.debugLevel >= 2) | ||
reason = `- hoisted dependency ${prettyPrintLocator(dep.locator)} has a clash with ${prettyPrintLocator(depNode.locator)} at ${reasonRoot}`; | ||
areRegularDepsSatisfied = false; | ||
} | ||
} | ||
if (!isHoistable) { | ||
if (!areRegularDepsSatisfied) { | ||
break; | ||
} | ||
} | ||
// Check that hoisted dependencies of unhoisted children are still satisifed | ||
if (isHoistable) { | ||
const checkChildren = (node) => { | ||
for (const dep of node.dependencies.values()) { | ||
if (node.originalDependencies.has(dep.name) && !node.peerNames.has(dep.name)) { | ||
for (const subDep of dep.hoistedDependencies.values()) { | ||
const rootDepNode = rootNode.dependencies.get(subDep.name) || rootNode.hoistedDependencies.get(subDep.name); | ||
if (!rootDepNode || rootDepNode.physicalLocator !== subDep.physicalLocator || !checkChildren(dep)) { | ||
return false; | ||
} | ||
} | ||
} | ||
} | ||
return true; | ||
}; | ||
isHoistable = checkChildren(node); | ||
} | ||
isHoistable = areRegularDepsSatisfied; | ||
} | ||
if (isHoistable) { | ||
for (const name of node.peerNames) { | ||
const parentDepNode = parentNode.dependencies.get(name); | ||
if (parentDepNode) { | ||
isHoistable = false; | ||
let arePeerDepsSatisfied = true; | ||
const checkList = new Set(node.peerNames); | ||
for (let idx = parents.length - 1; idx >= 0; idx--) { | ||
const parent = parents[idx].node; | ||
for (const name of checkList) { | ||
if (parent.peerNames.has(name)) | ||
continue; | ||
const parentDepNode = parent.dependencies.get(name); | ||
if (parentDepNode) { | ||
if (options.debugLevel >= 2) | ||
reason = `- peer dependency ${prettyPrintLocator(parentDepNode.locator)} from parent ${prettyPrintLocator(parent.locator)} was not hoisted to ${reasonRoot}`; | ||
arePeerDepsSatisfied = false; | ||
break; | ||
} | ||
checkList.delete(name); | ||
} | ||
if (!arePeerDepsSatisfied) { | ||
break; | ||
} | ||
} | ||
isHoistable = arePeerDepsSatisfied; | ||
} | ||
if (isHoistable) { | ||
let hoistCandidate = hoistCandidates.get(node.name); | ||
if (!hoistCandidate || (competitorInfo && competitorInfo.physicalLocator !== node.physicalLocator)) { | ||
hoistCandidate = { physicalLocator: node.physicalLocator, tuples: new Set(), weight }; | ||
if (!hoistCandidate || (competitorInfo && competitorInfo.node.ident !== node.ident)) { | ||
hoistCandidate = { node, candidates: new Set(), weight: weight }; | ||
hoistCandidates.set(node.name, hoistCandidate); | ||
} | ||
hoistCandidate.tuples.add({ parent: parentNode, node }); | ||
hoistCandidate.candidates.add({ nodePath, node }); | ||
} | ||
else if (options.debugLevel >= 2) { | ||
const parent = parents[parents.length - 1].node; | ||
const prevReason = parent.reasons.get(node.name); | ||
if (!prevReason || prevReason.root === rootNode) { | ||
parent.reasons.set(node.name, { reason: reason, root: rootNode }); | ||
} | ||
} | ||
if (!isSeen && locatorPath.indexOf(node.locator) < 0) { | ||
seenNodes.add(node); | ||
const parent = parents[parents.length - 1].node; | ||
const tuple = { parent, node }; | ||
parents.push(tuple); | ||
for (const dep of node.dependencies.values()) | ||
if (!node.peerNames.has(dep.name)) | ||
computeHoistCandidates([...nodePath, node], [...locatorPath, node.locator], dep); | ||
parents.pop(); | ||
} | ||
}; | ||
for (const dep of rootNode.dependencies.values()) { | ||
for (const subDep of dep.dependencies.values()) { | ||
computeHoistCandidates(dep, subDep); | ||
} | ||
if (rootNode.peerNames.has(dep.name) || dep.locator === rootNode.locator) | ||
continue; | ||
const tuple = { parent: rootNode, node: dep }; | ||
parents.push(tuple); | ||
for (const subDep of dep.dependencies.values()) | ||
if (!dep.peerNames.has(subDep.name) && subDep.locator !== dep.locator) | ||
computeHoistCandidates([dep], [rootNode.locator, dep.locator], subDep); | ||
parents.pop(); | ||
} | ||
const candidates = new Set(); | ||
for (const { tuples } of hoistCandidates.values()) | ||
for (const tuple of tuples) | ||
candidates.add(tuple); | ||
return candidates; | ||
return new Set(hoistCandidates.values()); | ||
}; | ||
const selfCheck = (tree) => { | ||
let log = []; | ||
const seenNodes = new Set(); | ||
const parents = new Set(); | ||
const checkNode = (node, parentDeps) => { | ||
if (seenNodes.has(node)) | ||
return; | ||
seenNodes.add(node); | ||
if (parents.has(node)) | ||
return; | ||
const dependencies = new Map(parentDeps); | ||
for (const dep of node.dependencies.values()) | ||
if (!node.peerNames.has(dep.name)) | ||
dependencies.set(dep.name, dep); | ||
for (const origDep of node.originalDependencies.values()) { | ||
const dep = dependencies.get(origDep.name); | ||
const prettyPrintTreePath = () => `${Array.from(parents).concat([node]).map(x => prettyPrintLocator(x.locator)).join('→')}`; | ||
if (node.peerNames.has(origDep.name)) { | ||
const parentDep = parentDeps.get(origDep.name); | ||
if (parentDep !== dep) { | ||
log.push(`${prettyPrintTreePath()} - broken peer promise: expected ${dep.locator} but found ${parentDep ? parentDep.locator : parentDep}`); | ||
} | ||
} | ||
else { | ||
if (!dep) { | ||
log.push(`${prettyPrintTreePath()} - broken require promise: no required dependency ${origDep.locator} found`); | ||
} | ||
else if (dep.ident !== origDep.ident) { | ||
log.push(`${prettyPrintTreePath()} - broken require promise: expected ${origDep.ident}, but found: ${dep.ident}`); | ||
} | ||
} | ||
} | ||
parents.add(node); | ||
for (const dep of node.dependencies.values()) { | ||
if (!node.peerNames.has(dep.name)) { | ||
checkNode(dep, dependencies); | ||
} | ||
} | ||
parents.delete(node); | ||
}; | ||
checkNode(tree, tree.dependencies); | ||
return log.join('\n'); | ||
}; | ||
/** | ||
@@ -280,3 +380,3 @@ * Creates a clone of package tree with extra fields used for hoisting purposes. | ||
locator: makeLocator(name, reference), | ||
physicalLocator: makePhysicalLocator(name, reference), | ||
ident: makeIdent(name, reference), | ||
dependencies: new Map(), | ||
@@ -286,8 +386,7 @@ originalDependencies: new Map(), | ||
peerNames: new Set(peerNames), | ||
reasons: new Map(), | ||
}; | ||
const seenNodes = new Map([[tree, treeCopy]]); | ||
const addNode = (origParent, node, parentNode) => { | ||
const addNode = (node, parentNode) => { | ||
// Skip self-references | ||
if (node === origParent) | ||
return; | ||
let workNode = seenNodes.get(node); | ||
@@ -301,3 +400,3 @@ const isSeen = !!workNode; | ||
locator: makeLocator(name, reference), | ||
physicalLocator: makePhysicalLocator(name, reference), | ||
ident: makeIdent(name, reference), | ||
dependencies: new Map(), | ||
@@ -307,2 +406,3 @@ originalDependencies: new Map(), | ||
peerNames: new Set(peerNames), | ||
reasons: new Map(), | ||
}; | ||
@@ -315,3 +415,3 @@ seenNodes.set(node, workNode); | ||
for (const dep of node.dependencies) { | ||
addNode(node, dep, workNode); | ||
addNode(dep, workNode); | ||
} | ||
@@ -321,3 +421,3 @@ } | ||
for (const dep of tree.dependencies) | ||
addNode(tree, dep, treeCopy); | ||
addNode(dep, treeCopy); | ||
return treeCopy; | ||
@@ -336,5 +436,5 @@ }; | ||
}; | ||
const nodes = new Map([[tree, treeCopy]]); | ||
const nodes = new Map([[tree.locator, treeCopy]]); | ||
const addNode = (node, parentNode) => { | ||
let resultNode = nodes.get(node); | ||
let resultNode = nodes.get(node.locator); | ||
const isSeen = !!resultNode; | ||
@@ -349,2 +449,3 @@ if (!resultNode) { | ||
if (!isSeen) { | ||
nodes.set(node.locator, resultNode); | ||
for (const dep of node.dependencies.values()) { | ||
@@ -371,16 +472,17 @@ if (!node.peerNames.has(dep.name)) { | ||
const ancestorMap = new Map(); | ||
const seenNodes = new Set(); | ||
const addParent = (parentNodes, node) => { | ||
const isSeen = seenNodes.has(node); | ||
seenNodes.add(node); | ||
let parents = ancestorMap.get(node.physicalLocator); | ||
const seenNodes = new Set([tree]); | ||
const addParent = (parentNode, node) => { | ||
const isSeen = !!seenNodes.has(node); | ||
let parents = ancestorMap.get(node.ident); | ||
if (!parents) { | ||
parents = new Set(); | ||
ancestorMap.set(node.physicalLocator, parents); | ||
ancestorMap.set(node.ident, parents); | ||
} | ||
for (const parent of parentNodes) | ||
parents.add(parent.physicalLocator); | ||
parents.add(parentNode.ident); | ||
if (!isSeen) { | ||
seenNodes.add(node); | ||
for (const dep of node.dependencies.values()) { | ||
addParent(new Set(parentNodes).add(node), dep); | ||
if (!node.peerNames.has(dep.name)) { | ||
addParent(node, dep); | ||
} | ||
} | ||
@@ -390,4 +492,59 @@ } | ||
for (const dep of tree.dependencies.values()) | ||
addParent(new Set([tree]), dep); | ||
if (!tree.peerNames.has(dep.name)) | ||
addParent(tree, dep); | ||
return ancestorMap; | ||
}; | ||
const prettyPrintLocator = (locator) => { | ||
const idx = locator.indexOf('@', 1); | ||
const name = locator.substring(0, idx); | ||
const reference = locator.substring(idx + 1); | ||
if (reference === 'workspace:.') { | ||
return `.`; | ||
} | ||
else if (!reference) { | ||
return `${name}`; | ||
} | ||
else { | ||
const version = (reference.indexOf('#') > 0 ? reference.split('#')[1] : reference).replace('npm:', ''); | ||
if (reference.startsWith('virtual')) { | ||
return `v:${name}@${version}`; | ||
} | ||
else { | ||
return `${name}@${version}`; | ||
} | ||
} | ||
}; | ||
const MAX_NODES_TO_DUMP = 50000; | ||
/** | ||
* Pretty-prints dependency tree in the `yarn why`-like format | ||
* | ||
* The function is used for troubleshooting purposes only. | ||
* | ||
* @param pkg node_modules tree | ||
* | ||
* @returns sorted node_modules tree | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const dumpDepTree = (tree) => { | ||
let nodeCount = 0; | ||
const dumpPackage = (pkg, parents, prefix = '') => { | ||
if (nodeCount > MAX_NODES_TO_DUMP || parents.has(pkg)) | ||
return ''; | ||
nodeCount++; | ||
const dependencies = Array.from(pkg.dependencies.values()); | ||
let str = ''; | ||
parents.add(pkg); | ||
for (let idx = 0; idx < dependencies.length; idx++) { | ||
const dep = dependencies[idx]; | ||
if (!pkg.peerNames.has(dep.name)) { | ||
const reasonObj = pkg.reasons.get(dep.name); | ||
str += `${prefix}${idx < dependencies.length - 1 ? '├─' : '└─'}${(parents.has(dep) ? '>' : '') + prettyPrintLocator(dep.locator) + (reasonObj ? ` ${reasonObj.reason}` : '')}\n`; | ||
str += dumpPackage(dep, parents, `${prefix}${idx < dependencies.length - 1 ? '│ ' : ' '}`); | ||
} | ||
} | ||
parents.delete(pkg); | ||
return str; | ||
}; | ||
const treeDump = dumpPackage(tree, new Set()); | ||
return treeDump + ((nodeCount > MAX_NODES_TO_DUMP) ? '\nTree is too large, part of the tree has been dunped\n' : ''); | ||
}; |
@@ -5,2 +5,3 @@ import { NodeModulesFS } from './NodeModulesFS'; | ||
export declare const patchFs: () => void; | ||
export { NodeModulesBaseNode, NodeModulesPackageNode, } from './buildNodeModulesTree'; | ||
export { NodeModulesFS, buildNodeModulesTree, buildLocatorMap, NodeModulesLocatorMap, getArchivePath, }; |
{ | ||
"name": "@yarnpkg/pnpify", | ||
"version": "2.0.0-rc.18", | ||
"version": "2.0.0-rc.19", | ||
"main": "./lib/index.js", | ||
@@ -9,3 +9,3 @@ "bin": "./lib/cli.js", | ||
"dependencies": { | ||
"@yarnpkg/fslib": "^2.0.0-rc.16", | ||
"@yarnpkg/fslib": "^2.0.0-rc.17", | ||
"chalk": "^3.0.0", | ||
@@ -20,4 +20,4 @@ "comment-json": "^2.2.0", | ||
"@yarnpkg/monorepo": "0.0.0", | ||
"@yarnpkg/pnp": "^2.0.0-rc.17", | ||
"eslint": "^5.16.0", | ||
"@yarnpkg/pnp": "^2.0.0-rc.18", | ||
"eslint": "^6.8.0", | ||
"typescript": "^3.7.5" | ||
@@ -24,0 +24,0 @@ }, |
84477
1925
Updated@yarnpkg/fslib@^2.0.0-rc.17