Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions workspaces/arborist/lib/can-place-dep.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class CanPlaceDep {
preferDedupe,
parent = null,
peerPath = [],
peerChecks = new Map(),
explicitRequest = false,
auditReport = null,
} = options
Expand Down Expand Up @@ -99,6 +100,7 @@ class CanPlaceDep {

// preventing cycles when we check peer sets
this.peerPath = peerPath
this.peerChecks = peerChecks
// we always prefer to dedupe peers, because they are trying
// a bit harder to be singletons.
this.preferDedupe = !!preferDedupe || edge.peer
Expand Down Expand Up @@ -387,12 +389,30 @@ class CanPlaceDep {
// of that dep needs to be placed shallower, because the target has
// a peer dep on the peer as well.
const target = deepestNestingTarget(this.target, peer.name)
const deepestTarget = deepestNestingTarget(this.deepestNestingTarget, peer.name)
// Dense peer graphs can reach the same placement through many paths.
// Each edge only needs one check for a given pair of placement targets.
let targetChecks = this.peerChecks.get(peerEdge)
if (!targetChecks) {
targetChecks = new Map()
this.peerChecks.set(peerEdge, targetChecks)
}
let deepestChecks = targetChecks.get(target)
if (!deepestChecks) {
deepestChecks = new Set()
targetChecks.set(target, deepestChecks)
}
if (deepestChecks.has(deepestTarget)) {
continue
}
deepestChecks.add(deepestTarget)
const cpp = new CanPlaceDep({
dep: peer,
target,
parent: this,
edge: peerEdge,
peerPath,
peerChecks: this.peerChecks,
auditReport: this.auditReport,
// always place peers in preferDedupe mode
preferDedupe: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ exports[`test/can-place-dep.js TAP basic placement check tests cycle of peers ha
Array []
`

exports[`test/can-place-dep.js TAP basic placement check tests dense peer graph > conflict children 1`] = `
Array []
`

exports[`test/can-place-dep.js TAP basic placement check tests do not keep existing dep that matches, but does not satisfy > conflict children 1`] = `
Array []
`
Expand Down
30 changes: 30 additions & 0 deletions workspaces/arborist/test/can-place-dep.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ t.test('basic placement check tests', t => {
explicitRequest,
// an audit report, telling us which nodes are vulnerable
auditReport,
// upper bound for the number of peer placement checks
maxChecks,
}) => {
const target = tree.inventory.get(targetLoc)
const node = tree.inventory.get(nodeLoc)
Expand Down Expand Up @@ -87,6 +89,9 @@ t.test('basic placement check tests', t => {
if (expectSelf) {
t.equal(cpd.canPlaceSelf, expectSelf, msg)
}
if (maxChecks) {
t.ok(cpd.allChildren.length <= maxChecks, 'avoids duplicate peer placement checks')
}
t.equal(cpd.description, cpd.canPlace.description || cpd.canPlace)
t.matchSnapshot([...cpd.conflictChildren].map(c => ({
dep: [c.dep.name, c.dep.version],
Expand Down Expand Up @@ -621,6 +626,31 @@ t.test('basic placement check tests', t => {
],
})

const densePeerNames = Array.from({ length: 8 }, (_, index) => `peer-${index}`)
const densePeerPackage = name => ({
name,
version: '1.0.0',
peerDependencies: Object.fromEntries(
densePeerNames.filter(peer => peer !== name).map(peer => [peer, '1'])
),
})
runTest('dense peer graph', {
tree: new Node({
path,
pkg: {
name: 'project',
version: '1.2.3',
dependencies: { [densePeerNames[0]]: '1' },
},
}),
targetLoc: '',
nodeLoc: '',
dep: new Node({ pkg: densePeerPackage(densePeerNames[0]) }),
expect: OK,
peerSet: densePeerNames.slice(1).map(name => ({ pkg: densePeerPackage(name) })),
maxChecks: densePeerNames.length ** 2,
})

runTest('peers with peerConflicted edges in peerSet', {
tree: new Node({
path,
Expand Down