Skip to content
Merged
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: 7 additions & 3 deletions workspaces/arborist/lib/arborist/isolated-reifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ module.exports = cls => class IsolatedReifier extends cls {
queue.push(edge.to)
}
})
if (!next.isProjectRoot && !next.isWorkspace && !next.inert) {
// local `file:` deps are in fsChildren but are not workspaces.
// they are already handled as workspace-like proxies above and should not go through the external/store extraction path.
if (!next.isProjectRoot && !next.isWorkspace && !next.inert && !idealTree.fsChildren.has(next) && !idealTree.fsChildren.has(next.target)) {
this.idealGraph.external.push(await this.externalProxy(next))
}
}
Expand Down Expand Up @@ -220,9 +222,11 @@ module.exports = cls => class IsolatedReifier extends cls {
}
}

// 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.
const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n)
const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target)
result.localDependencies = await Promise.all(nonOptionalDeps.filter(n => n.isWorkspace).map(n => this.workspaceProxy(n)))
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace && !n.inert).map(n => this.externalProxy(n)))
result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.workspaceProxy(n)))
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.externalProxy(n)))
result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(n => this.externalProxy(n)))
result.dependencies = [
...result.externalDependencies,
Expand Down
43 changes: 43 additions & 0 deletions workspaces/arborist/test/isolated-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,49 @@ tap.test('bins are installed', async t => {
t.ok(binFromBarToWhich)
})

tap.test('file: dependency with linked strategy', async t => {
/*
* Regression test for https://github.com/npm/cli/issues/7549
*
* 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.
*/
const graph = {
registry: [],
root: {
name: 'project1',
version: '1.0.0',
dependencies: { project2: 'file:./project2' },
},
}

const { dir, registry } = await getRepo(graph)

// Create the local file: dependency on disk
const depDir = path.join(dir, 'project2')
fs.mkdirSync(depDir, { recursive: true })
fs.writeFileSync(path.join(depDir, 'package.json'), JSON.stringify({
name: 'project2',
version: '1.0.0',
}))
fs.writeFileSync(path.join(depDir, 'index.js'), "module.exports = 'project2'")

const cache = fs.mkdtempSync(`${getTempDir()}/test-`)
const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache })
await arborist.reify({ installStrategy: 'linked' })

// The file dep should be symlinked in node_modules
const linkPath = path.join(dir, 'node_modules', 'project2')
const stat = fs.lstatSync(linkPath)
t.ok(stat.isSymbolicLink(), 'project2 is a symlink in node_modules')

// The symlink should resolve to the actual local directory
const realpath = fs.realpathSync(linkPath)
t.equal(realpath, depDir, 'symlink points to the correct local directory')

// The package should be requireable
t.ok(setupRequire(dir)('project2'), 'project2 can be required from root')
})

function setupRequire (cwd) {
return function requireChain (...chain) {
return chain.reduce((path, name) => {
Expand Down
Loading