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
10 changes: 8 additions & 2 deletions workspaces/arborist/lib/arborist/load-actual.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const calcDepFlags = require('../calc-dep-flags.js')
const Node = require('../node.js')
const Link = require('../link.js')
const realpath = require('../realpath.js')
const relpath = require('../relpath.js')
const PackageExtensions = require('../package-extensions.js')
const NpmExtension = require('../npm-extension.js')

Expand Down Expand Up @@ -361,8 +362,13 @@ module.exports = cls => class ActualLoader extends cls {
async #newLink (options) {
const { realpath } = options
this.#topNodes.add(realpath)
const target = this.#cache.get(realpath)
const link = new Link({ ...options, target })
// On Windows, links can spell one target differently (e.g. `D:\pkg` and `d:\pkg`).
// Those spellings share an inventory location, so a second target node there would evict
// the first and leave its links without a target. Reuse the node already at that location.
const root = options.root || options.parent?.root
const target = root?.inventory.get(relpath(root.realpath, realpath)) ||
this.#cache.get(realpath)
const link = new Link({ ...options, realpath: target?.path || realpath, target })

if (!target) {
// Link set its target itself in this case
Expand Down
34 changes: 34 additions & 0 deletions workspaces/arborist/test/arborist/load-actual.js
Original file line number Diff line number Diff line change
Expand Up @@ -934,3 +934,37 @@ t.test('a project located under a .store path still loads its own devDependencie
t.equal(tree.isInStore, false, 'loaded root is not flagged isInStore')
t.ok(tree.edgesOut.get('a-dev-dep'), 'root devDependency is a required edge')
})

t.test('links that spell one target with different drive-letter case share it', {
skip: process.platform !== 'win32' && 'drive letters are Windows-only',
}, async t => {
// Windows junctions keep the target spelling they were created with (npm/cli#9079).
const path = t.testdir({
pkg: { 'package.json': JSON.stringify({ name: 'pkg', version: '1.0.0' }) },
app: {
'package.json': JSON.stringify({
name: 'app',
version: '1.0.0',
dependencies: { a: 'file:../pkg', b: 'file:../pkg', c: 'file:../pkg' },
}),
node_modules: {},
},
})
const pkg = resolve(path, 'pkg')
const flipped = pkg[0] === pkg[0].toUpperCase()
? pkg[0].toLowerCase() + pkg.slice(1)
: pkg[0].toUpperCase() + pkg.slice(1)
for (const [name, target] of [['a', pkg], ['b', flipped], ['c', pkg]]) {
fs.symlinkSync(target, resolve(path, 'app/node_modules', name), 'junction')
}

const tree = await loadActual(resolve(path, 'app'))
const links = ['a', 'b', 'c'].map(name => tree.children.get(name))
const [target] = links.map(link => link.target)
t.ok(target, 'links have a target')
t.equal(target.root, tree, 'the target is in the tree')
for (const link of links) {
t.equal(link.target, target, `${link.name} shares the target`)
}
t.equal(target.linksIn.size, 3, 'the target knows all three links')
})