diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index ce1daec1a36a8..ae8094536e8b3 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -825,7 +825,14 @@ module.exports = cls => class Reifier extends cls { // symlink const dir = dirname(node.path) const target = node.realpath - const rel = relative(dir, target) + + let rel + if (node.resolved?.startsWith('file:')) { + rel = this.#calculateRelativePath(node, dir, target, nm) + } else { + rel = relative(dir, target) + } + await mkdir(dir, { recursive: true }) return symlink(rel, node.path, 'junction') } @@ -843,6 +850,36 @@ module.exports = cls => class Reifier extends cls { }) : p).then(() => node) } + #calculateRelativePath (node, dir, target) { + // Check if the node is affected by a root override + let hasRootOverride = [...node.edgesIn].some(edge => edge.from.isRoot && edge.overrides) + // If not set via edges, see if the root package.json explicitly lists an override + if (!hasRootOverride && node.root) { + const rootPackage = node.root.target + hasRootOverride = !!(rootPackage && + rootPackage.package.overrides && + rootPackage.package.overrides[node.name]) + } + if (!hasRootOverride) { + return relative(dir, target) + } + // If an override is detected, attempt to retrieve the override spec from the root package.json + const overrideSpec = node.root?.target?.package?.overrides?.[node.name] + if (typeof overrideSpec === 'string' && overrideSpec.startsWith('file:')) { + const overridePath = overrideSpec.replace(/^file:/, '') + const rootDir = node.root.target.path + return relative(dir, resolve(rootDir, overridePath)) + } + + // Fallback: derive the package name from node.resolved in a platform-agnostic way + const filePath = node.resolved.replace(/^file:/, '') + // A node.package.name could be different than the folder name + const pathParts = filePath.split(/[\\/]/) + const packageName = pathParts[pathParts.length - 1] + + return join('..', packageName) + } + #registryResolved (resolved) { // the default registry url is a magic value meaning "the currently // configured registry". diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index a8941fd44ae06..e1bc64e8d2850 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -8,6 +8,7 @@ const fs = require('node:fs') const fsp = require('node:fs/promises') const npmFs = require('@npmcli/fs') const MockRegistry = require('@npmcli/mock-registry') +const { dirname, relative } = require('node:path') let failRm = false let failRename = null @@ -3204,6 +3205,57 @@ t.test('installLinks', (t) => { t.end() }) +t.test('root overrides with file: paths are visible to workspaces', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + workspaces: ['hello'], + dependencies: {}, + overrides: { + print: 'file:./print', + }, + }), + hello: { + 'package.json': JSON.stringify({ + name: 'hello', + version: '1.0.0', + dependencies: { + print: '../print', + }, + }), + }, + print: { + 'package.json': JSON.stringify({ + name: 'print', + version: '1.0.0', + main: 'index.js', + }), + }, + }) + + createRegistry(t, false) + await reify(path) + + const printSymlink = fs.readlinkSync(resolve(path, 'node_modules/print')) + + // Create a platform-agnostic way to compare symlink targets + const normalizeLinkTarget = target => { + if (process.platform === 'win32') { + // For Windows: convert absolute paths to relative and normalize slashes + const linkDir = dirname(resolve(path, 'node_modules/print')) + return relative(linkDir, target).replace(/\\/g, '/') + } + // For Unix: already a relative path + return target + } + + t.equal( + normalizeLinkTarget(printSymlink), + '../print', + 'print symlink points to ../print (normalized for platform)' + ) +}) + t.test('should preserve exact ranges, missing actual tree', async (t) => { const Pacote = require('pacote') const Arborist = t.mock('../../lib/arborist', {