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
39 changes: 38 additions & 1 deletion workspaces/arborist/lib/arborist/reify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand All @@ -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".
Expand Down
52 changes: 52 additions & 0 deletions workspaces/arborist/test/arborist/reify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', {
Expand Down
Loading