Skip to content

Commit 51365b1

Browse files
fix(arborist): update store symlinks when hash changes in linked strategy (#9107)
1 parent 8e0a731 commit 51365b1

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

workspaces/arborist/lib/arborist/reify.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,10 @@ module.exports = cls => class Reifier extends cls {
817817
if (combined.has(child.path) || !existsSync(child.path)) {
818818
continue
819819
}
820+
// Skip store links whose ideal realpath doesn't exist on disk yet — the store hash changed and the symlink needs recreating via ADD.
821+
if (child.isLink && child.resolved?.startsWith('file:.store/') && !existsSync(child.realpath)) {
822+
continue
823+
}
820824
let entry
821825
if (child.isLink) {
822826
entry = new IsolatedLink(child)

workspaces/arborist/test/isolated-mode.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,6 +1979,53 @@ tap.test('orphaned store entries are cleaned up on dependency removal', async t
19791979
'all store entries are removed when dependencies are removed')
19801980
})
19811981

1982+
tap.test('store symlinks are updated when hash changes after adding a dep', async t => {
1983+
const graph = {
1984+
registry: [
1985+
{ name: 'which', version: '1.0.0', dependencies: { isexe: '^1.0.0' } },
1986+
{ name: 'isexe', version: '1.0.0' },
1987+
{ name: 'abbrev', version: '2.0.0' },
1988+
],
1989+
root: {
1990+
name: 'myproject',
1991+
version: '1.0.0',
1992+
dependencies: { which: '1.0.0' },
1993+
},
1994+
}
1995+
const { dir, registry } = await getRepo(graph)
1996+
const cache = fs.mkdtempSync(`${getTempDir()}/test-`)
1997+
1998+
// First install — only which
1999+
const arb1 = new Arborist({ path: dir, registry, packumentCache: new Map(), cache })
2000+
await arb1.reify({ installStrategy: 'linked' })
2001+
2002+
const whichLink = path.join(dir, 'node_modules', 'which')
2003+
t.ok(fs.lstatSync(whichLink).isSymbolicLink(), 'which is a symlink after first install')
2004+
const hashBefore = fs.readlinkSync(whichLink)
2005+
2006+
// Add abbrev — changes the dep graph, causing store hash recalculation
2007+
const pkgPath = path.join(dir, 'package.json')
2008+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
2009+
pkg.dependencies.abbrev = '2.0.0'
2010+
fs.writeFileSync(pkgPath, JSON.stringify(pkg))
2011+
2012+
const arb2 = new Arborist({ path: dir, registry, packumentCache: new Map(), cache })
2013+
await arb2.reify({ installStrategy: 'linked' })
2014+
2015+
// The symlink target should still be valid (not dangling)
2016+
t.ok(fs.existsSync(whichLink), 'which symlink target exists after adding abbrev')
2017+
t.ok(setupRequire(dir)('which'), 'which is requireable after adding abbrev')
2018+
2019+
// Verify the symlink was updated if the hash changed
2020+
const hashAfter = fs.readlinkSync(whichLink)
2021+
if (hashBefore !== hashAfter) {
2022+
const storeDir = path.join(dir, 'node_modules', '.store')
2023+
const storeEntries = fs.readdirSync(storeDir)
2024+
const oldKey = hashBefore.split('/')[0].replace('.store/', '')
2025+
t.notOk(storeEntries.includes(oldKey), 'old store entry was cleaned up')
2026+
}
2027+
})
2028+
19822029
function setupRequire (cwd) {
19832030
return function requireChain (...chain) {
19842031
return chain.reduce((path, name) => {

0 commit comments

Comments
 (0)