// this test depends on debug stuff, so force it on, even if the test env // does not enable it. process.env.ARBORIST_DEBUG = '1' const t = require('tap') const CanPlaceDep = require('../lib/can-place-dep.js') const { CONFLICT, OK, REPLACE, KEEP, } = CanPlaceDep const Node = require('../lib/node.js') t.test('basic placement check tests', t => { const path = '/some/path' // boilerplate so we can define a bunch of test cases declaratively const runTest = (desc, { // the tree we're asking to add something into tree, // the target we're asking to add it in targetLoc, // the location of the node with the dependency nodeLoc, // the dep being added dep, // the expected overall result for the dep and its peers expect, // the expected result for the dep itself, ignoring peers expectSelf, // --prefer-dedupe set? preferDedupe, // array of nodes representing the dep's peer group peerSet, // is this dep the thing the user is explicitly installing? explicitRequest, }) => { const target = tree.inventory.get(targetLoc) const node = tree.inventory.get(nodeLoc) const edge = node.edgesOut.get(dep.name) if (!dep.satisfies(edge)) { edge.peerConflicted = true } const vr = new Node({ sourceReference: node, path: node.path, pkg: { ...node.package }, children: peerSet, }) dep.parent = vr // mark any invalid edges in the virtual root as peerConflicted for (const child of vr.children.values()) { for (const edgeIn of child.edgesIn) { if (edgeIn.invalid) { edgeIn.peerConflicted = true } } } const msg = `place ${ dep.package._id } in ${targetLoc || 'ROOT'} for { ${ edge.from.location || 'ROOT' } ${ edge.type + ' ' + edge.name + '@' + edge.spec } }` t.test(desc, t => { const cpd = new CanPlaceDep({ target, edge, dep, preferDedupe, explicitRequest, }) // dump a comment if the assertion fails. // would put it in the diags, but yaml stringifies Set objects // super awkwardly, and Node objects have a lot of those. if (!t.equal(cpd.canPlace, expect, msg)) { t.comment(cpd) } if (expectSelf) { t.equal(cpd.canPlaceSelf, expectSelf, msg) } t.equal(cpd.description, cpd.canPlace.description || cpd.canPlace) t.matchSnapshot([...cpd.conflictChildren].map(c => ({ dep: [c.dep.name, c.dep.version], edge: [c.edge.from.location, c.edge.type, c.edge.name, c.edge.spec], canPlace: c.canPlace, canPlaceSelf: c.canPlaceSelf, })), 'conflict children') t.end() }) } runTest('basic placement of a dep, no conflicts or issues', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3' }, }), expect: OK, }) runTest('replace an existing dep', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, children: [{ pkg: { name: 'a', version: '1.0.0' } }], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3' } }), expect: REPLACE, }) runTest('place nested', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, children: [ { pkg: { name: 'a', version: '1.0.0' } }, { pkg: { name: 'b', version: '1.0.0', dependencies: { a: '2.x' } }, }, ], }), targetLoc: 'node_modules/b', nodeLoc: 'node_modules/b', dep: new Node({ pkg: { name: 'a', version: '2.3.4' } }), expect: OK, }) runTest('conflict in root for nested dep', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, children: [ { pkg: { name: 'a', version: '1.0.0' } }, { pkg: { name: 'b', version: '1.0.0', dependencies: { a: '2' } } }, ], }), targetLoc: '', nodeLoc: 'node_modules/b', dep: new Node({ pkg: { name: 'a', version: '2.3.4' } }), expect: CONFLICT, }) runTest('conflict in root for nested dep, no current', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, children: [ { pkg: { name: 'b', version: '1.0.0', dependencies: { a: '2' } } }, ], }), targetLoc: '', nodeLoc: 'node_modules/b', dep: new Node({ pkg: { name: 'a', version: '2.3.4' } }), expect: CONFLICT, }) runTest('keep an existing dep that matches', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1' } }, children: [ { pkg: { name: 'a', version: '1.2.3' } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3' } }), expect: KEEP, }) runTest('do not keep existing dep that matches, but does not satisfy', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: 'foo/bar' } }, children: [ { pkg: { name: 'a', version: '1.2.3' } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3' }, resolved: 'git+ssh://github.com/foo/bar', }), expect: REPLACE, }) runTest('keep existing dep that matches, does not satisfy, but peerConflicted', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '2.3.4' } }, children: [ { pkg: { name: 'a', version: '1.2.3' } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3' } }), expect: KEEP, }) // https://github.com/npm/cli/issues/3411 runTest('replace an existing dep that matches, explicit request', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1' } }, children: [ { pkg: { name: 'a', version: '1.2.3' } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3' } }), expect: REPLACE, explicitRequest: true, }) runTest('replace an existing dep that could dedupe, explicit request', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '*', b: '1.2.3', } }, children: [ { pkg: { name: 'a', version: '1.2.3' } }, { pkg: { name: 'b', version: '1.2.3', dependencies: { a: '1.2.3' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '2.3.4' } }), expect: REPLACE, explicitRequest: true, }) runTest('keep an existing dep that could dedupe, explicit request, preferDedupe', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '*', b: '1.2.3', } }, children: [ { pkg: { name: 'a', version: '1.2.3' } }, { pkg: { name: 'b', version: '1.2.3', dependencies: { a: '1.2.3' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '2.3.4' } }), expect: KEEP, preferDedupe: true, explicitRequest: true, }) runTest('keep an existing dep that is older, but also works', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1', c: '2.0.0' } }, children: [ { pkg: { name: 'b', version: '2.0.0' } }, { pkg: { name: 'c', version: '2.0.0', dependencies: { b: '2.0.0' } } }, { pkg: { name: 'a', version: '1.2.3', dependencies: { b: '2' } } }, ], }), targetLoc: '', nodeLoc: 'node_modules/a', dep: new Node({ pkg: { name: 'b', version: '2.3.4' } }), expect: KEEP, }) runTest('replace an existing dep that is newer, because preferDedupe', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1', c: '2.0.0' } }, children: [ { pkg: { name: 'b', version: '2.3.4' } }, { pkg: { name: 'c', version: '2.0.0', dependencies: { b: '2.0.0' } } }, { pkg: { name: 'a', version: '1.2.3', dependencies: { b: '2' } } }, ], }), targetLoc: '', nodeLoc: 'node_modules/c', dep: new Node({ pkg: { name: 'b', version: '2.0.0' } }), expect: REPLACE, preferDedupe: true, }) runTest('conflict an existing dep that is newer, preferDedupe peerConflict', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1', c: '2.0.0' } }, children: [ { pkg: { name: 'b', version: '2.3.4' } }, { pkg: { name: 'c', version: '2.0.0', dependencies: { b: '2.0.0' } } }, { pkg: { name: 'a', version: '1.2.3', dependencies: { b: '2' } } }, ], }), targetLoc: '', nodeLoc: 'node_modules/c', dep: new Node({ pkg: { name: 'b', version: '2.0.0', peerDependencies: { a: '3' } }, }), peerSet: [ { pkg: { name: 'a', version: '3.0.0' } }, ], expect: CONFLICT, preferDedupe: true, }) runTest('conflict an existing dep that is newer, because no preferDedupe', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1', c: '2.0.0' } }, children: [ { pkg: { name: 'b', version: '2.3.4' } }, { pkg: { name: 'c', version: '2.0.0', dependencies: { b: '2.0.0' } } }, { pkg: { name: 'a', version: '1.2.3', dependencies: { b: '2' } } }, ], }), targetLoc: '', nodeLoc: 'node_modules/c', dep: new Node({ pkg: { name: 'b', version: '2.0.0' } }), expect: CONFLICT, preferDedupe: false, }) // always OK or REPLACE if the dep being placed had errors runTest('return REPLACE because node had errors', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1' } }, children: [ { pkg: { name: 'a', version: '1.2.3' } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3' }, error: new Error('uh oh'), }), expect: REPLACE, }) runTest('return OK because node had errors', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, children: [ { pkg: { name: 'b', version: '1.0.0', peerDependencies: { a: '2.x' } }, }, ], }), targetLoc: '', nodeLoc: 'node_modules/b', dep: new Node({ pkg: { name: 'a', version: '2.3.4' }, error: new Error('uh oh'), }), expect: OK, }) runTest('cannot place peer inside of dependent', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { b: '1.x' } }, children: [ { pkg: { name: 'b', version: '1.0.0', peerDependencies: { a: '2.x' } }, }, ], }), targetLoc: 'node_modules/b', nodeLoc: 'node_modules/b', dep: new Node({ pkg: { name: 'a', version: '2.3.4' } }), expect: CONFLICT, }) // root -> (x) // x -> (a, b@1) // a -> (b@2) // b@1 -> (c@1) // b@2 -> (c@2) // // root // +-- c@1 // +-- x // +-- a // | +-- b@2 // +-- b@1 // place c@2 in x, CONFLICT due to shadowing runTest('invalid shadowing', { tree: new Node({ path, pkg: { dependencies: { x: '1' } }, children: [ { pkg: { name: 'c', version: '1.0.0' } }, { pkg: { name: 'x', version: '1.0.0', dependencies: { a: '1', b: '1' } }, children: [ { pkg: { name: 'a', version: '1.0.0', dependencies: { b: '2' } }, children: [ { pkg: { name: 'b', version: '2.0.0', dependencies: { c: '2' } } }, ], }, { pkg: { name: 'b', version: '1.0.0', dependencies: { c: '1' } } }, ], }, ], }), targetLoc: 'node_modules/x', nodeLoc: 'node_modules/x/node_modules/a/node_modules/b', dep: new Node({ pkg: { name: 'c', version: '2.0.0' } }), expect: CONFLICT, }) runTest('totally valid shadowing', { tree: new Node({ path, pkg: { dependencies: { x: '1' } }, children: [ { pkg: { name: 'c', version: '1.0.0' } }, { pkg: { name: 'x', version: '1.0.0', dependencies: { a: '1', b: '1' } }, children: [ { pkg: { name: 'a', version: '1.0.0', dependencies: { b: '2' } }, children: [ { pkg: { name: 'b', version: '2.0.0', dependencies: { c: '2' } } }, ], }, // difference right here on the c@1||2 line { pkg: { name: 'b', version: '1.0.0', dependencies: { c: '1||2' } } }, ], }, ], }), targetLoc: 'node_modules/x', nodeLoc: 'node_modules/x/node_modules/a/node_modules/b', dep: new Node({ pkg: { name: 'c', version: '2.0.0' } }), expect: OK, }) // peer dep shenanigans runTest('basic placement with peers', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3', peerDependencies: { b: '1' } }, }), expect: OK, peerSet: [ { pkg: { name: 'b', version: '1.2.3' } }, ], }) runTest('peer with peers', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3', peerDependencies: { b: '1' } }, }), expect: OK, peerSet: [ { pkg: { name: 'b', version: '1.2.3', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.2.3' } }, ], }) runTest('cycle of peers', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3', peerDependencies: { b: '1' } }, }), expect: OK, peerSet: [ { pkg: { name: 'b', version: '1.2.3', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.2.3', peerDependencies: { a: '1' } } }, ], }) runTest('cycle of peers hanging off entry node', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3', peerDependencies: { b: '1' } }, }), expect: OK, peerSet: [ { pkg: { name: 'b', version: '1.2.3', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.2.3', peerDependencies: { d: '1' } } }, { pkg: { name: 'd', version: '1.2.3', peerDependencies: { b: '1' } } }, ], }) runTest('peers with peerConflicted edges in peerSet', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x' } }, }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3', peerDependencies: { b: '1' } }, }), expect: OK, peerSet: [ { pkg: { name: 'b', version: '1.2.3', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.2.3', peerDependencies: { b: '2', a: '2' } } }, ], }) runTest('peers with peerConflicted edges in peerSet from dependent', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', c: '2.x', }, }, children: [ { pkg: { name: 'c', version: '2.0.1', peerDependencies: { b: '2', a: '2' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3', peerDependencies: { b: '1' } }, }), expect: OK, peerSet: [ { pkg: { name: 'b', version: '1.2.3', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '2.2.3', peerDependencies: { b: '2', a: '2' } } }, ], }) runTest('replacing existing peer set', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', c: '2.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', peerDependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '2.0.1', peerDependencies: { b: '2', a: '2' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'a', version: '1.2.3', peerDependencies: { b: '1' } }, }), expect: REPLACE, peerSet: [ { pkg: { name: 'b', version: '1.2.3', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '2.2.3', peerDependencies: { b: '2', a: '2' } } }, ], explicitRequest: true, }) runTest('existing peer set which can be pushed deeper, no current', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '2.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', dependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.1' } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'd', version: '2.2.2', peerDependencies: { b: '2' } }, }), expect: OK, peerSet: [ { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], explicitRequest: true, }) runTest('existing peer set which can be pushed deeper, with invalid current', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '2.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', dependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.1' } }, { pkg: { name: 'd', version: '1.1.1', peerDependencies: { b: '1' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'd', version: '2.2.2', peerDependencies: { b: '2' } }, }), expect: REPLACE, peerSet: [ { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], explicitRequest: true, }) // root -> (a@1, d@1) // a@1.0.1 -> (b@1) // b@1.0.1 -> PEER(c@1) // d@1.1.1 -> PEER(b@1) // d@1.2.2 -> PEER(b@2) // b@2.2.2 -> PEER(c@2) // // root // +-- a@1.0.1 // +-- b@1.0.1 <-- can be pushed under a, along with c & d // +-- c@1.0.1 // +-- d@1.0.1 // PLACE(d@1.2.2) runTest('existing peer set which can be pushed deeper, with valid current', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '1.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', dependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.1' } }, { pkg: { name: 'd', version: '1.1.1', peerDependencies: { b: '1' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'd', version: '1.2.2', peerDependencies: { b: '2' } }, }), expect: REPLACE, peerSet: [ { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], explicitRequest: true, }) runTest('existing peer set which can be pushed deeper, conflict on peer', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '1.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', dependencies: { bb: '1' } } }, { pkg: { name: 'bb', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.1' } }, { pkg: { name: 'd', version: '1.1.1', peerDependencies: { c: '1' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'd', version: '1.2.2', peerDependencies: { b: '2' } }, }), expect: REPLACE, peerSet: [ { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], explicitRequest: true, }) runTest('existing peer set cannot be pushed deeper, but new dep set can', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '2.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', dependencies: { b: '1' } } }, { pkg: { name: 'd', version: '2.2.2', peerDependencies: { b: '2' } } }, { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], }), targetLoc: '', nodeLoc: 'node_modules/a', dep: new Node({ pkg: { name: 'b', version: '1.0.1', peerDependencies: { c: '1' } }, }), expect: CONFLICT, peerSet: [ { pkg: { name: 'c', version: '1.0.1' } }, ], explicitRequest: true, }) runTest('nest peer set under dependent node', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '2.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', dependencies: { b: '1' } } }, { pkg: { name: 'd', version: '2.2.2', peerDependencies: { b: '2' } } }, { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], }), targetLoc: 'node_modules/a', nodeLoc: 'node_modules/a', dep: new Node({ pkg: { name: 'b', version: '1.0.1', peerDependencies: { c: '1' } }, }), expect: OK, peerSet: [ { pkg: { name: 'c', version: '1.0.1' } }, ], explicitRequest: true, }) runTest('existing peer set cannot be pushed deeper, neither can new set', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '2.x', }, }, children: [ { pkg: { name: 'a', version: '1.0.1', peerDependencies: { b: '1' } } }, { pkg: { name: 'd', version: '2.2.2', peerDependencies: { b: '2' } } }, { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], }), targetLoc: '', nodeLoc: 'node_modules/a', dep: new Node({ pkg: { name: 'b', version: '1.0.1', peerDependencies: { c: '1' } }, }), expect: CONFLICT, peerSet: [ { pkg: { name: 'c', version: '1.0.1' } }, ], explicitRequest: true, }) runTest('existing peer set cannot be pushed deeper, neither can new set, conflict on peer', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '1.x', }, }, children: [ // the bb dep could be nested, but it has a peerDep on c, and // a would be fine with the c@2, but can't nest its c@1 dep { pkg: { name: 'a', version: '1.0.1', dependencies: { bb: '1' }, peerDependencies: { c: '*' } } }, { pkg: { name: 'bb', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.1' } }, { pkg: { name: 'd', version: '1.1.1', peerDependencies: { c: '1' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'd', version: '1.2.2', peerDependencies: { b: '2' } }, }), expect: CONFLICT, peerSet: [ { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], explicitRequest: true, }) // root -> (a@1, d@1) // a@1.0.1 -> (bb@1), PEER(c) // bb@1.0.1 -> (cc@1), PEER(c) // cc@1.0.1 -> PEER(dd@1) // dd@1.0.1 -> PEER(c@1) // d@1.0.1 -> PEER(c) // d@1.2.2 -> PEER(b@2) // b@2.2.2 -> PEER(c@2) // // root // +-- a@1.0.1 // +-- bb@1.0.1 // +-- cc@1.0.1 // +-- dd@1.0.1 // +-- c@1.0.1 // +-- d@1.1.1 // // PLACE d@1.2.2 in root. peerSet(b@2, c@2) // REPLACE d@1.0.1 // OK b@2 (no current, no conflicting edges) // c@1.0.1 -> c@2.2.2? // entry edges: // root->(a): no replacement // a->(c): replacement satisfies // a->(bb): not a peer edge // >> can replace // a->(bb): // bb->(c): replacement satisfies // bb->(cc): not a peer edge // root->(d): is cpd peerEntryEdge, skip // bb->(cc): no replacement // dd->(cc): not a peer edge // >> can replace runTest('existing peer set cannot be pushed deeper, neither can new set, conflict on deep peer', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '1.x', }, }, children: [ // the bb dep could be nested, but it has a peerDep on c, and // a would be fine with the c@2, but can't nest its c@1 dep { pkg: { name: 'a', version: '1.0.1', dependencies: { bb: '1' }, peerDependencies: { c: '*' } } }, { pkg: { name: 'bb', version: '1.0.1', dependencies: { cc: '1' }, peerDependencies: { c: '*' } } }, { pkg: { name: 'cc', version: '1.0.1', peerDependencies: { dd: '1' } } }, { pkg: { name: 'dd', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.1' } }, { pkg: { name: 'd', version: '1.1.1', peerDependencies: { c: '1' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'd', version: '1.2.2', peerDependencies: { b: '2' } }, }), expect: CONFLICT, peerSet: [ { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.2.2' } }, ], explicitRequest: true, }) runTest('existing peer set cannot be pushed deeper, neither can new set, replacement satisfies', { tree: new Node({ path, pkg: { name: 'project', version: '1.2.3', dependencies: { a: '1.x', d: '1.x', }, }, children: [ // the bb dep could be nested, but it has a peerDep on c, and // a would be fine with the c@2, but can't nest its c@1 dep { pkg: { name: 'a', version: '1.0.1', dependencies: { bb: '1' }, peerDependencies: { c: '*' } } }, { pkg: { name: 'bb', version: '1.0.1', dependencies: { cc: '1' }, peerDependencies: { c: '*' } } }, { pkg: { name: 'cc', version: '1.0.1', dependencies: { dd: '1' }, peerDependencies: { c: '*' } } }, { pkg: { name: 'dd', version: '1.0.1', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.1' } }, { pkg: { name: 'd', version: '1.1.1', peerDependencies: { c: '1' } } }, ], }), targetLoc: '', nodeLoc: '', dep: new Node({ pkg: { name: 'd', version: '1.2.2', peerDependencies: { b: '2' } }, }), expect: REPLACE, peerSet: [ { pkg: { name: 'b', version: '2.2.2', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.2.2' } }, ], explicitRequest: true, }) // root -> (a) // a -> (b, c) PEER(p) // b -> (c@1, d@2) PEER(p) // c@1 -> (d@1) PEER(p) // c@2 -> (d@2) PEER(p) // d@1 -> PEER(p@1) // d@2 -> PEER(p@2) // root // +-- a // +-- b // | +-- c@1 // | +-- d@1 <-- cannot place p@1 peer dep! // +-- p@2 // +-- c@2 // +-- d@2 runTest('peer all the way down, conflict but not ours', { tree: new Node({ path, pkg: { dependencies: { a: '' } }, children: [ { pkg: { name: 'a', version: '1.0.0', dependencies: { b: '', c: '' }, peerDependencies: { p: '' }, }, }, { pkg: { name: 'b', version: '1.0.0', dependencies: { c: '1', d: '2' }, peerDependencies: { p: '' }, }, children: [ { pkg: { name: 'c', version: '1.0.0', dependencies: { d: '1' }, // <-- the dep we'll try to place peerDependencies: { p: '' }, }, }, ], }, { pkg: { name: 'p', version: '2.0.0' } }, { pkg: { name: 'c', version: '2.0.0', peerDependencies: { p: '' } } }, { pkg: { name: 'd', version: '2.0.0', peerDependencies: { p: '2' } } }, ], }), dep: new Node({ pkg: { name: 'd', version: '1.0.0', peerDependencies: { p: '1' } }, }), peerSet: [{ pkg: { name: 'p', version: '1.0.0' } }], nodeLoc: 'node_modules/b/node_modules/c', targetLoc: 'node_modules/b/node_modules/c', expect: CONFLICT, expectSelf: OK, }) // root -> (a@1, b@2) // a@1 -> PEER(b@1) // b@1 -> PEER(c@1) // c@1 -> PEER(d@1) // d@1 -> PEER(e@1) // e@1 -> PEER(a@1) // a@2 -> PEER(b@2) // b@2 -> PEER(c@2) // c@2 -> PEER(d@2) // d@2 -> PEER(e@2) // e@2 -> PEER(a@2) // root // +-- a@1 // +-- b@1 // +-- c@1 // +-- d@1 // +-- e@1 // place b@2, peerSet (c@2, d@2, e@2, a@2) runTest('prod dep directly on conflicted peer, newer', { tree: new Node({ path, pkg: { dependencies: { a: '1', b: '2' } }, children: [ { pkg: { name: 'a', version: '1.0.0', peerDependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.0', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.0', peerDependencies: { d: '1' } } }, { pkg: { name: 'd', version: '1.0.0', peerDependencies: { e: '1' } } }, { pkg: { name: 'e', version: '1.0.0', peerDependencies: { a: '1' } } }, ], }), dep: new Node({ pkg: { name: 'b', version: '2.0.0', peerDependencies: { c: '2' } }, }), peerSet: [ { pkg: { name: 'a', version: '2.0.0', peerDependencies: { b: '2' } } }, { pkg: { name: 'c', version: '2.0.0', peerDependencies: { d: '2' } } }, { pkg: { name: 'd', version: '2.0.0', peerDependencies: { e: '2' } } }, { pkg: { name: 'e', version: '2.0.0', peerDependencies: { a: '2' } } }, ], nodeLoc: '', targetLoc: '', expect: CONFLICT, expectSelf: REPLACE, }) runTest('prod dep directly on conflicted peer, older', { tree: new Node({ path, pkg: { dependencies: { a: '2', b: '1' } }, children: [ { pkg: { name: 'a', version: '2.0.0', peerDependencies: { b: '2' } } }, { pkg: { name: 'b', version: '2.0.0', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.0.0', peerDependencies: { d: '2' } } }, { pkg: { name: 'd', version: '2.0.0', peerDependencies: { e: '2' } } }, { pkg: { name: 'e', version: '2.0.0', peerDependencies: { a: '2' } } }, ], }), dep: new Node({ pkg: { name: 'b', version: '1.0.0', peerDependencies: { c: '1' } }, }), peerSet: [ { pkg: { name: 'a', version: '1.0.0', peerDependencies: { b: '1' } } }, { pkg: { name: 'c', version: '1.0.0', peerDependencies: { d: '1' } } }, { pkg: { name: 'd', version: '1.0.0', peerDependencies: { e: '1' } } }, { pkg: { name: 'e', version: '1.0.0', peerDependencies: { a: '1' } } }, ], nodeLoc: '', targetLoc: '', expect: CONFLICT, expectSelf: REPLACE, }) // root -> (c@1||2, a@2) // a@1 -> PEER(b@1) // a@2 -> PEER(b@2) // b@1 -> PEER(c@1) // b@2 -> PEER(c@2) // c@1 -> PEER(d@1) // c@2 -> PEER(d@2) // // root // +-- a@1 // +-- b@1 // +-- c@1 // +-- d@1 // // place a@2 peerSet(b@2, c@2, d@2) // // peer group (c@1, d@1) can be replaced, because the entry node c has a // valid replacement. runTest('have replacement for conflicted entry node', { tree: new Node({ path, pkg: { dependencies: { a: '2', c: '1||2' } }, children: [ { pkg: { name: 'a', version: '1.0.0', peerDependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.0', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.0', peerDependencies: { d: '1' } } }, { pkg: { name: 'd', version: '1.0.0' } }, ], }), dep: new Node({ pkg: { name: 'a', version: '2.0.0', peerDependencies: { b: '2' } }, }), peerSet: [ { pkg: { name: 'b', version: '2.0.0', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.0.0', peerDependencies: { d: '2' } } }, { pkg: { name: 'd', version: '2.0.0' } }, ], nodeLoc: '', targetLoc: '', expect: REPLACE, expectSelf: REPLACE, }) // v@4 -> PEER(a@1||2) // y@1 -> PEER(d@1) // a@1 -> PEER(b@1) // b@1 -> PEER(c@1) // c@1 -> PEER(d@1) // d@1 -> PEER(e@1) // e@1 -> PEER(a@1) // a@2 -> PEER(b@2) // b@2 -> PEER(c@2) // c@2 -> PEER(d@2) // d@2 -> PEER(e@2) // e@2 -> PEER(a@2) // // root // +-- v@4 // +-- a@2 // +-- b@2 // +-- c@2 // +-- d@2 // +-- e@2 // // place y@1 (a@1, b@1, c@1, d@1, e@1), OK, because all peers replaced runTest('replacing overlapping peer sets', { tree: new Node({ path, pkg: { dependencies: { v: '4', y: '1' } }, children: [ { pkg: { name: 'v', version: '4.0.0', peerDependencies: { a: '1||2', x: '2' }, peerDependenciesMeta: { x: { optional: true } }, }, }, { pkg: { name: 'a', version: '2.0.0', peerDependencies: { b: '2' } } }, { pkg: { name: 'b', version: '2.0.0', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.0.0', peerDependencies: { d: '2' } } }, { pkg: { name: 'd', version: '2.0.0', peerDependencies: { e: '2' } } }, { pkg: { name: 'e', version: '2.0.0', peerDependencies: { a: '2' } } }, ], }), dep: new Node({ pkg: { name: 'y', version: '1.0.0', peerDependencies: { d: '1' } }, }), peerSet: [ { pkg: { name: 'a', version: '1.0.0', peerDependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.0', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.0', peerDependencies: { d: '1' } } }, { pkg: { name: 'd', version: '1.0.0', peerDependencies: { e: '1' } } }, { pkg: { name: 'e', version: '1.0.0', peerDependencies: { a: '1' } } }, ], nodeLoc: '', targetLoc: '', expect: OK, }) // same as above, but the new peer set only overlaps _part_ of the existing // v@4 -> PEER(a@1||2) // y@1 -> PEER(d@1) // a@1 -> PEER(c@1) // c@1 -> PEER(e@1) // e@1 -> PEER(a@1) // a@2 -> PEER(b@2) // b@2 -> PEER(c@2) // c@2 -> PEER(d@2) // d@2 -> PEER(e@2) // e@2 -> PEER(a@2) // // root // +-- v@4 // +-- a@2 // +-- b@2 // +-- c@2 // +-- d@2 // +-- e@2 // // place y@1 (a@1, c@1, e@1), OK, because all peers replaced runTest('replacing partially overlapping peer sets, subset', { tree: new Node({ path, pkg: { dependencies: { v: '4', y: '1' } }, children: [ { pkg: { name: 'v', version: '4.0.0', peerDependencies: { a: '1||2', x: '2' }, peerDependenciesMeta: { x: { optional: true } }, }, }, { pkg: { name: 'a', version: '2.0.0', peerDependencies: { b: '2' } } }, { pkg: { name: 'b', version: '2.0.0', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.0.0', peerDependencies: { d: '2' } } }, { pkg: { name: 'd', version: '2.0.0', peerDependencies: { e: '2' } } }, { pkg: { name: 'e', version: '2.0.0', peerDependencies: { a: '2' } } }, ], }), dep: new Node({ pkg: { name: 'y', version: '1.0.0', peerDependencies: { d: '1' } }, }), peerSet: [ { pkg: { name: 'a', version: '1.0.0', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.0', peerDependencies: { e: '1' } } }, { pkg: { name: 'e', version: '1.0.0', peerDependencies: { a: '1' } } }, ], nodeLoc: '', targetLoc: '', expect: OK, }) // same as above, but now the existing one has 3, replacment has 5 // v@4 -> PEER(a@1||2) // y@1 -> PEER(d@1) // a@1 -> PEER(b@1) // b@1 -> PEER(c@1) // c@1 -> PEER(d@1) // d@1 -> PEER(e@1) // e@1 -> PEER(a@1) // a@2 -> PEER(c@2) // c@2 -> PEER(e@2) // e@2 -> PEER(a@2) // // root // +-- v@4 // +-- a@2 // +-- c@2 // +-- e@2 // // place y@1 (a@1, b@1, c@1, d@1, e@1), OK, because all peers replaced runTest('replacing partially overlapping peer sets, superset', { tree: new Node({ path, pkg: { dependencies: { v: '4', y: '1' } }, children: [ { pkg: { name: 'v', version: '4.0.0', peerDependencies: { a: '1||2', x: '2' }, peerDependenciesMeta: { x: { optional: true } }, }, }, { pkg: { name: 'a', version: '2.0.0', peerDependencies: { c: '2' } } }, { pkg: { name: 'c', version: '2.0.0', peerDependencies: { e: '2' } } }, { pkg: { name: 'e', version: '2.0.0', peerDependencies: { a: '2' } } }, ], }), dep: new Node({ pkg: { name: 'y', version: '1.0.0', peerDependencies: { d: '1' } }, }), peerSet: [ { pkg: { name: 'a', version: '1.0.0', peerDependencies: { b: '1' } } }, { pkg: { name: 'b', version: '1.0.0', peerDependencies: { c: '1' } } }, { pkg: { name: 'c', version: '1.0.0', peerDependencies: { d: '1' } } }, { pkg: { name: 'd', version: '1.0.0', peerDependencies: { e: '1' } } }, { pkg: { name: 'e', version: '1.0.0', peerDependencies: { a: '1' } } }, ], nodeLoc: '', targetLoc: '', expect: OK, }) // partly overlapping peer sets that diverge // v -> PEER(a@1||2, x@1) // a@1 -> PEER(c@1, d@1) // a@2 -> PEER(c@2, e@1) // x@1 -> PEER(y@1) // y@1 -> PEER(a@1||2) // w -> PEER(a@1, j@1) // j@1 -> PEER(y@1) // root // +-- v // +-- a@2 // +-- c@2 // +-- e@1 // +-- x@1 // +-- y@1 // place w(a@1, j@1, y@1, c@1, d@1), OK runTest('replacing partially overlapping divergent peer sets', { tree: new Node({ path, pkg: { dependencies: { v: '', w: '' } }, children: [ { pkg: { name: 'v', version: '1.0.0', peerDependencies: { a: '1||2', x: '1' }, }, }, { pkg: { name: 'a', version: '2.0.0', peerDependencies: { c: '2', e: '1' } } }, { pkg: { name: 'c', version: '2.0.0' } }, { pkg: { name: 'e', version: '1.0.0' } }, { pkg: { name: 'x', version: '1.0.0', peerDependencies: { y: '1' } } }, { pkg: { name: 'y', version: '1.0.0', peerDependencies: { a: '1||2' } } }, ], }), dep: new Node({ pkg: { name: 'w', version: '1.0.0', peerDependencies: { a: '1', j: '1' } }, }), peerSet: [ { pkg: { name: 'a', version: '1.0.0', peerDependencies: { c: '1', d: '1' } } }, { pkg: { name: 'j', version: '1.0.0', peerDependencies: { y: '1' } } }, { pkg: { name: 'y', version: '1.0.0', peerDependencies: { a: '1||2' } } }, { pkg: { name: 'c', version: '1.0.0' } }, { pkg: { name: 'd', version: '1.0.0' } }, ], nodeLoc: '', targetLoc: '', expect: OK, }) // root -> (k, y@1) // k -> (x) // x -> PEER(y@1||2) // // root // +-- y@1 // +-- k@1 // // place x in root with y@2 in peerset // https://github.com/npm/cli/issues/3881 runTest('can dedupe, cannot place peer', { tree: new Node({ path, pkg: { dependencies: { k: '1', y: '1' } }, children: [ { pkg: { name: 'y', version: '1.0.0' } }, { pkg: { name: 'k', version: '1.0.0', dependencies: { x: '' } } }, ], }), dep: new Node({ pkg: { name: 'x', version: '1.0.0', peerDependencies: { y: '1||2' } }, }), peerSet: [ { pkg: { name: 'y', version: '2.0.0' } }, ], targetLoc: '', nodeLoc: 'node_modules/k', expect: OK, }) t.end() }) t.test('constructor debug throws', t => { t.throws(() => new CanPlaceDep({}), { message: 'no dep provided to CanPlaceDep', }) t.throws(() => new CanPlaceDep({ dep: new Node({ pkg: { name: 'x', version: '1.2.3' } }), }), { message: 'no target provided to CanPlaceDep', }) t.throws(() => new CanPlaceDep({ dep: new Node({ pkg: { name: 'x', version: '1.2.3' } }), target: new Node({ path: '/some/path' }), }), { message: 'no edge provided to CanPlaceDep', }) t.end() })