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
21 changes: 17 additions & 4 deletions workspaces/arborist/lib/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -1238,22 +1238,35 @@ This is a one-time fix-up, please be patient...
// Check if the target is within the project root
isProjectInternalFileSpec = targetPath.startsWith(resolvedProjectRoot + sep) || targetPath === resolvedProjectRoot
}

// When using --install-links, we need to handle transitive file dependencies specially
// If the parent was installed (not linked) due to --install-links, and this is a file: dep, we should also install it rather than link it
const parentWasInstalled = parent && !parent.isLink && parent.resolved?.startsWith('file:')
const isTransitiveFileDep = spec.type === 'directory' && parentWasInstalled && installLinks

// Decide whether to link or copy the dependency
const shouldLink = isWorkspace || isProjectInternalFileSpec || !installLinks
const shouldLink = (isWorkspace || isProjectInternalFileSpec || !installLinks) && !isTransitiveFileDep
if (spec.type === 'directory' && shouldLink) {
return this.#linkFromSpec(name, spec, parent, edge)
}

// if the spec matches a workspace name, then see if the workspace node will
// satisfy the edge. if it does, we return the workspace node to make sure it
// takes priority.
// if the spec matches a workspace name, then see if the workspace node will satisfy the edge. if it does, we return the workspace node to make sure it takes priority.
if (isWorkspace) {
const existingNode = this.idealTree.edgesOut.get(spec.name).to
if (existingNode && existingNode.isWorkspace && existingNode.satisfies(edge)) {
return existingNode
}
}

// For file: dependencies that we're installing (not linking), ensure proper resolution
if (isTransitiveFileDep && edge) {
// For transitive file deps, resolve relative to the parent's original source location
const parentOriginalPath = parent.resolved.slice(5) // Remove 'file:' prefix
const relativePath = edge.rawSpec.slice(5) // Remove 'file:' prefix
const absolutePath = resolve(parentOriginalPath, relativePath)
spec = npa.resolve(name, `file:${absolutePath}`)
}

// spec isn't a directory, and either isn't a workspace or the workspace we have
// doesn't satisfy the edge. try to fetch a manifest and build a node from that.
return this.#fetchManifest(spec)
Expand Down
60 changes: 60 additions & 0 deletions workspaces/arborist/test/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -4389,4 +4389,64 @@ t.test('installLinks behavior with project-internal file dependencies', async t
t.ok(nestedDep, 'nested-dep should be found')
t.ok(nestedDep.isLink, 'nested-dep should be a link (project-internal)')
})

t.test('installLinks=true with transitive external file dependencies', async t => {
// mainpkg installs b (external file dep) with --install-links
// b depends on a (another external file dep via file:../a)
// Both should be installed (not linked) and dependencies should resolve correctly
const testRoot = t.testdir({
a: {
'package.json': JSON.stringify({
name: 'a',
main: 'index.js',
}),
'index.js': 'export const A = "A";',
},
b: {
'package.json': JSON.stringify({
name: 'b',
main: 'index.js',
dependencies: {
a: 'file:../a',
},
}),
'index.js': 'import {A} from "a";export const fn = () => console.log(A);',
},
mainpkg: {
'package.json': JSON.stringify({}),
},
})

const mainpkgPath = join(testRoot, 'mainpkg')
const bPath = join(testRoot, 'b')
createRegistry(t, false)

const arb = newArb(mainpkgPath, { installLinks: true })

// Add the external file dependency using the full path
await arb.buildIdealTree({ add: [`file:${bPath}`] })

const tree = arb.idealTree

// Both packages should be present in the tree
const packageB = tree.children.get('b')
const packageA = tree.children.get('a')

t.ok(packageB, 'package b should be found in tree')
t.ok(packageA, 'package a should be found in tree (transitive dependency)')

// Both should be installed (not linked) due to installLinks=true
t.notOk(packageB.isLink, 'package b should not be a link (installLinks=true)')
t.notOk(packageA.isLink, 'package a should not be a link (transitive with installLinks=true)')

// Verify that the resolved paths are correct
t.match(packageB.resolved, /file:.*[/\\]b$/, 'package b should have correct resolved path')
t.match(packageA.resolved, /file:.*[/\\]a$/, 'package a should have correct resolved path')

// Verify the dependency relationship
const edgeToA = packageB.edgesOut.get('a')
t.ok(edgeToA, 'package b should have an edge to a')
t.ok(edgeToA.valid, 'the edge from b to a should be valid')
t.equal(edgeToA.to, packageA, 'the edge from b should point to package a')
})
})
Loading