diff --git a/.changeset/common-crabs-push.md b/.changeset/common-crabs-push.md new file mode 100644 index 0000000000..a24688934a --- /dev/null +++ b/.changeset/common-crabs-push.md @@ -0,0 +1,6 @@ +--- +"@pnpm/resolve-dependencies": patch +"pnpm": patch +--- + +When automatically installing missing peer dependencies, prefer versions that are already present in the direct dependencies of the root workspace package [#9835](https://github.com/pnpm/pnpm/pull/9835). diff --git a/pkg-manager/core/test/install/peerDependencies.ts b/pkg-manager/core/test/install/peerDependencies.ts index ac7b72ef33..1d5c2b523f 100644 --- a/pkg-manager/core/test/install/peerDependencies.ts +++ b/pkg-manager/core/test/install/peerDependencies.ts @@ -331,6 +331,85 @@ test('peer dependency is resolved from the dependencies of the workspace root pr } }) +test('peer dependency is resolved from the dependencies of the workspace root project even if there are other versions of the peer dependency present in the dependency graph', async () => { + const projects = preparePackages([ + { + location: '.', + package: { name: 'root' }, + }, + { + location: 'pkg', + package: {}, + }, + { + location: 'pkg2', + package: {}, + }, + ]) + const allProjects: ProjectOptions[] = [ + { + buildIndex: 0, + manifest: { + name: 'root', + version: '1.0.0', + + dependencies: { + ajv: '4.10.0', + }, + }, + rootDir: process.cwd() as ProjectRootDir, + }, + { + buildIndex: 0, + manifest: { + name: 'pkg', + version: '1.0.0', + + dependencies: { + 'ajv-keywords': '1.5.0', + }, + }, + rootDir: path.resolve('pkg') as ProjectRootDir, + }, + { + buildIndex: 0, + manifest: { + name: 'pkg2', + version: '1.0.0', + + dependencies: { + ajv: '5.0.0', + }, + }, + rootDir: path.resolve('pkg2') as ProjectRootDir, + }, + ] + const reporter = jest.fn() + await mutateModules([ + { + mutation: 'install', + rootDir: process.cwd() as ProjectRootDir, + }, + { + mutation: 'install', + rootDir: path.resolve('pkg') as ProjectRootDir, + }, + { + mutation: 'install', + rootDir: path.resolve('pkg2') as ProjectRootDir, + }, + ], testDefaults({ allProjects, reporter, resolvePeersFromWorkspaceRoot: true })) + + expect(reporter).not.toHaveBeenCalledWith(expect.objectContaining({ + name: 'pnpm:peer-dependency-issues', + })) + + { + const lockfile = projects.root.readLockfile() + expect(lockfile.importers.pkg?.dependencies?.['ajv-keywords'].version).toBe('1.5.0(ajv@4.10.0)') + } +}) + test('warning is reported when cannot resolve peer dependency for non-top-level dependency', async () => { prepareEmpty() await addDistTag({ package: '@pnpm.e2e/abc-parent-with-ab', version: '1.0.0', distTag: 'latest' }) diff --git a/pkg-manager/resolve-dependencies/src/hoistPeers.ts b/pkg-manager/resolve-dependencies/src/hoistPeers.ts index 57929ac1d5..dea5eff7f5 100644 --- a/pkg-manager/resolve-dependencies/src/hoistPeers.ts +++ b/pkg-manager/resolve-dependencies/src/hoistPeers.ts @@ -1,15 +1,22 @@ import { type PreferredVersions } from '@pnpm/resolver-base' import semver from 'semver' +import { type PkgAddressOrLink } from './resolveDependencies' export function hoistPeers ( - missingRequiredPeers: Array<[string, { range: string }]>, opts: { autoInstallPeers: boolean allPreferredVersions?: PreferredVersions - } + workspaceRootDeps: PkgAddressOrLink[] + }, + missingRequiredPeers: Array<[string, { range: string }]> ): Record { const dependencies: Record = {} for (const [peerName, { range }] of missingRequiredPeers) { + const rootDep = opts.workspaceRootDeps.find((rootDep) => rootDep.alias === peerName) + if (rootDep?.version) { + dependencies[peerName] = rootDep.version + continue + } if (opts.allPreferredVersions![peerName]) { const versions: string[] = [] const nonVersions: string[] = [] diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts index de998d4512..72e182c05f 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencies.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencies.ts @@ -145,6 +145,7 @@ export interface ResolutionContext { forceFullResolution: boolean ignoreScripts?: boolean resolvedPkgsById: ResolvedPkgsById + resolvePeersFromWorkspaceRoot?: boolean outdatedDependencies: Record childrenByParentId: ChildrenByParentId patchedDependencies?: PatchGroupRecord @@ -320,6 +321,18 @@ export async function resolveRootDependencies ( time, } } + let workspaceRootDeps!: PkgAddressOrLink[] + if (ctx.resolvePeersFromWorkspaceRoot) { + const rootImporterIndex = importers.findIndex(({ options }) => options.parentIds[0] === '.') + workspaceRootDeps = pkgAddressesByImportersWithoutPeers[rootImporterIndex]?.pkgAddresses ?? [] + } else { + workspaceRootDeps = [] + } + const _hoistPeers = hoistPeers.bind(null, { + autoInstallPeers: ctx.autoInstallPeers, + allPreferredVersions: ctx.allPreferredVersions, + workspaceRootDeps, + }) /* eslint-disable no-await-in-loop */ while (true) { const allMissingOptionalPeersByImporters = await Promise.all(pkgAddressesByImportersWithoutPeers.map(async (importerResolutionResult, index) => { @@ -357,7 +370,7 @@ export async function resolveRootDependencies ( } } if (!missingRequiredPeers.length) break - const dependencies = hoistPeers(missingRequiredPeers, ctx) + const dependencies = _hoistPeers(missingRequiredPeers) if (!Object.keys(dependencies).length) break const wantedDependencies = getNonDevWantedDependencies({ dependencies }) diff --git a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts index 69dd6055e6..54024d9cc3 100644 --- a/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts +++ b/pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts @@ -180,6 +180,7 @@ export async function resolveDependencyTree ( readPackageHook: opts.hooks.readPackage, registries: opts.registries, resolvedPkgsById: {} as ResolvedPkgsById, + resolvePeersFromWorkspaceRoot: opts.resolvePeersFromWorkspaceRoot, resolutionMode: opts.resolutionMode, skipped: wantedToBeSkippedPackageIds, storeController: opts.storeController, diff --git a/pkg-manager/resolve-dependencies/test/hoistPeers.test.ts b/pkg-manager/resolve-dependencies/test/hoistPeers.test.ts index 7b3253ccb3..3d507378e6 100644 --- a/pkg-manager/resolve-dependencies/test/hoistPeers.test.ts +++ b/pkg-manager/resolve-dependencies/test/hoistPeers.test.ts @@ -1,14 +1,15 @@ import { hoistPeers, getHoistableOptionalPeers } from '../lib/hoistPeers' test('hoistPeers picks an already available prerelease version', () => { - expect(hoistPeers([['foo', { range: '*' }]], { + expect(hoistPeers({ autoInstallPeers: false, allPreferredVersions: { foo: { '1.0.0-beta.0': 'version', }, }, - })).toStrictEqual({ + workspaceRootDeps: [], + }, [['foo', { range: '*' }]])).toStrictEqual({ foo: '1.0.0-beta.0', }) })