Skip to content

Commit 16fbe13

Browse files
fix: resolve relative file: dependencies correctly with install-strategy=linked (#9030)
1 parent 983742b commit 16fbe13

File tree

2 files changed

+50
-3
lines changed

2 files changed

+50
-3
lines changed

workspaces/arborist/lib/arborist/isolated-reifier.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ module.exports = cls => class IsolatedReifier extends cls {
113113
queue.push(edge.to)
114114
}
115115
})
116-
if (!next.isProjectRoot && !next.isWorkspace && !next.inert) {
116+
// local `file:` deps are in fsChildren but are not workspaces.
117+
// they are already handled as workspace-like proxies above and should not go through the external/store extraction path.
118+
if (!next.isProjectRoot && !next.isWorkspace && !next.inert && !idealTree.fsChildren.has(next) && !idealTree.fsChildren.has(next.target)) {
117119
this.idealGraph.external.push(await this.externalProxy(next))
118120
}
119121
}
@@ -220,9 +222,11 @@ module.exports = cls => class IsolatedReifier extends cls {
220222
}
221223
}
222224

225+
// local `file:` deps (non-workspace fsChildren) should be treated as local dependencies, not external, so they get symlinked directly instead of being extracted into the store.
226+
const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n)
223227
const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target)
224-
result.localDependencies = await Promise.all(nonOptionalDeps.filter(n => n.isWorkspace).map(n => this.workspaceProxy(n)))
225-
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace && !n.inert).map(n => this.externalProxy(n)))
228+
result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.workspaceProxy(n)))
229+
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.externalProxy(n)))
226230
result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(n => this.externalProxy(n)))
227231
result.dependencies = [
228232
...result.externalDependencies,

workspaces/arborist/test/isolated-mode.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,6 +1736,49 @@ tap.test('bins are installed', async t => {
17361736
t.ok(binFromBarToWhich)
17371737
})
17381738

1739+
tap.test('file: dependency with linked strategy', async t => {
1740+
/*
1741+
* Regression test for https://github.com/npm/cli/issues/7549
1742+
*
1743+
* A relative file: dependency (file:./project2) was incorrectly resolved as file:../project2, causing ENOENT errors because the path was resolved one level above the project root.
1744+
*/
1745+
const graph = {
1746+
registry: [],
1747+
root: {
1748+
name: 'project1',
1749+
version: '1.0.0',
1750+
dependencies: { project2: 'file:./project2' },
1751+
},
1752+
}
1753+
1754+
const { dir, registry } = await getRepo(graph)
1755+
1756+
// Create the local file: dependency on disk
1757+
const depDir = path.join(dir, 'project2')
1758+
fs.mkdirSync(depDir, { recursive: true })
1759+
fs.writeFileSync(path.join(depDir, 'package.json'), JSON.stringify({
1760+
name: 'project2',
1761+
version: '1.0.0',
1762+
}))
1763+
fs.writeFileSync(path.join(depDir, 'index.js'), "module.exports = 'project2'")
1764+
1765+
const cache = fs.mkdtempSync(`${getTempDir()}/test-`)
1766+
const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache })
1767+
await arborist.reify({ installStrategy: 'linked' })
1768+
1769+
// The file dep should be symlinked in node_modules
1770+
const linkPath = path.join(dir, 'node_modules', 'project2')
1771+
const stat = fs.lstatSync(linkPath)
1772+
t.ok(stat.isSymbolicLink(), 'project2 is a symlink in node_modules')
1773+
1774+
// The symlink should resolve to the actual local directory
1775+
const realpath = fs.realpathSync(linkPath)
1776+
t.equal(realpath, depDir, 'symlink points to the correct local directory')
1777+
1778+
// The package should be requireable
1779+
t.ok(setupRequire(dir)('project2'), 'project2 can be required from root')
1780+
})
1781+
17391782
function setupRequire (cwd) {
17401783
return function requireChain (...chain) {
17411784
return chain.reduce((path, name) => {

0 commit comments

Comments
 (0)