feat: support resolving peers with npm aliases (#6220)

close #4301
Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Rex Zeng
2023-03-22 10:39:11 +08:00
committed by GitHub
parent f53a29d76f
commit cfb6bb3bfe
23 changed files with 452 additions and 475 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/resolve-dependencies": patch
"pnpm": patch
---
Aliased packages should be used to resolve peer dependencies too [#4301](https://github.com/pnpm/pnpm/issues/4301).

View File

@@ -44,7 +44,7 @@
"@pnpm/constants": "workspace:*",
"@pnpm/lockfile-types": "workspace:*",
"@pnpm/modules-yaml": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/types": "workspace:*",
"is-windows": "^1.0.2",
"isexe": "2.0.0",

View File

@@ -41,7 +41,7 @@
},
"dependencies": {
"@pnpm/cafs": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"path-exists": "^4.0.0"
},
"devDependencies": {

View File

@@ -38,7 +38,7 @@
"@pnpm/filter-workspace-packages": "workspace:*",
"@pnpm/plugin-commands-rebuild": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/ramda": "0.28.20",
"@types/semver": "7.3.13",

View File

@@ -37,7 +37,7 @@
"@pnpm/filter-workspace-packages": "workspace:*",
"@pnpm/plugin-commands-script-runners": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@types/is-windows": "^1.0.0",
"@types/ramda": "0.28.20",
"is-windows": "^1.0.2",

View File

@@ -39,7 +39,7 @@
"@commitlint/prompt-cli": "^17.4.4",
"@pnpm/eslint-config": "workspace:*",
"@pnpm/meta-updater": "0.2.2",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/tsconfig": "workspace:*",
"@types/jest": "^29.4.0",
"@types/node": "^14.18.37",

View File

@@ -36,7 +36,7 @@
"@pnpm/filter-workspace-packages": "workspace:*",
"@pnpm/plugin-commands-patching": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/normalize-path": "^3.0.0",
"@types/ramda": "0.28.20",

View File

@@ -78,7 +78,7 @@
"@pnpm/lockfile-types": "workspace:*",
"@pnpm/package-store": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/store-path": "workspace:*",
"@pnpm/test-fixtures": "workspace:*",
"@types/fs-extra": "^9.0.13",

View File

@@ -1388,3 +1388,66 @@ test('deduplicate packages that have peers, when adding new dependency in a work
expect(depPaths).toContain(`/@pnpm.e2e/abc@1.0.0${createPeersFolderSuffix([{ name: '@pnpm.e2e/peer-a', version: '1.0.0' }, { name: '@pnpm.e2e/peer-b', version: '1.0.0' }, { name: '@pnpm.e2e/peer-c', version: '1.0.0' }])}`)
expect(depPaths).toContain(`/@pnpm.e2e/abc-parent-with-ab@1.0.0${createPeersFolderSuffix([{ name: '@pnpm.e2e/peer-c', version: '1.0.0' }])}`)
})
test('resolve peer dependencies from aliased subdependencies if they are dependencies of a parent package', async () => {
prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/peer-a', version: '1.0.0', distTag: 'latest' })
await addDistTag({ package: '@pnpm.e2e/peer-c', version: '1.0.0', distTag: 'latest' })
await addDependenciesToPackage(
{},
['@pnpm.e2e/abc-parent-with-aliases'],
await testDefaults({ autoInstallPeers: false, strictPeerDependencies: false })
)
const lockfile = await readYamlFile<any>(path.resolve(WANTED_LOCKFILE)) // eslint-disable-line
expect(lockfile.packages['/@pnpm.e2e/abc@1.0.0(@pnpm.e2e/peer-a@1.0.0)(@pnpm.e2e/peer-b@1.0.0)(@pnpm.e2e/peer-c@1.0.0)']).toBeTruthy()
})
test('resolve peer dependency from aliased direct dependency', async () => {
prepareEmpty()
const opts = await testDefaults({ autoInstallPeers: false, strictPeerDependencies: false })
const manifest = await addDependenciesToPackage({}, ['peer-a@npm:@pnpm.e2e/peer-a@1.0.0'], opts)
await addDependenciesToPackage(manifest, ['@pnpm.e2e/abc@1.0.0'], opts)
const lockfile = await readYamlFile<any>(path.resolve(WANTED_LOCKFILE)) // eslint-disable-line
expect(lockfile.packages['/@pnpm.e2e/abc@1.0.0(@pnpm.e2e/peer-a@1.0.0)']).toBeTruthy()
})
test('resolve peer dependency using the alias that differs from the real name of the direct dependency', async () => {
prepareEmpty()
const opts = await testDefaults({ autoInstallPeers: false, strictPeerDependencies: false })
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/peer-b@npm:@pnpm.e2e/peer-a@1.0.0'], opts)
await addDependenciesToPackage(manifest, ['@pnpm.e2e/abc@1.0.0'], opts)
const lockfile = await readYamlFile<any>(path.resolve(WANTED_LOCKFILE)) // eslint-disable-line
expect(lockfile.packages['/@pnpm.e2e/abc@1.0.0(@pnpm.e2e/peer-a@1.0.0)(@pnpm.e2e/peer-a@1.0.0)']).toBeTruthy()
expect(lockfile.packages['/@pnpm.e2e/abc@1.0.0(@pnpm.e2e/peer-a@1.0.0)(@pnpm.e2e/peer-a@1.0.0)']?.dependencies['@pnpm.e2e/peer-a']).toBe('1.0.0')
expect(lockfile.packages['/@pnpm.e2e/abc@1.0.0(@pnpm.e2e/peer-a@1.0.0)(@pnpm.e2e/peer-a@1.0.0)']?.dependencies['@pnpm.e2e/peer-b']).toBe('/@pnpm.e2e/peer-a@1.0.0')
})
test('when there are several aliased dependencies of the same package, pick the one with the highest version to resolve peers', async () => {
prepareEmpty()
const opts = await testDefaults({ autoInstallPeers: false, strictPeerDependencies: false })
const manifest = await addDependenciesToPackage({}, [
'peer-c3@npm:@pnpm.e2e/peer-c@1.0.0',
'peer-c2@npm:@pnpm.e2e/peer-c@1.0.1',
'peer-c1@npm:@pnpm.e2e/peer-c@2.0.0',
], opts)
await addDependenciesToPackage(manifest, ['@pnpm.e2e/abc@1.0.0'], opts)
const lockfile = await readYamlFile<any>(path.resolve(WANTED_LOCKFILE)) // eslint-disable-line
expect(lockfile.packages['/@pnpm.e2e/abc@1.0.0(@pnpm.e2e/peer-c@2.0.0)']).toBeTruthy()
})
test('in a subdependency, when there are several aliased dependencies of the same package, pick the one with the highest version to resolve peers', async () => {
prepareEmpty()
await addDependenciesToPackage({}, ['@pnpm.e2e/abc-parent-with-aliases-of-same-pkg@1.0.0'], await testDefaults({ autoInstallPeers: false, strictPeerDependencies: false }))
const lockfile = await readYamlFile<any>(path.resolve(WANTED_LOCKFILE)) // eslint-disable-line
expect(lockfile.packages['/@pnpm.e2e/abc@1.0.0(@pnpm.e2e/peer-c@2.0.0)']).toBeTruthy()
})

View File

@@ -22,7 +22,7 @@
"@pnpm/package-store": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/read-projects-context": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/store-path": "workspace:*",
"@pnpm/test-fixtures": "workspace:*",
"@types/fs-extra": "^9.0.13",

View File

@@ -68,7 +68,7 @@
"@pnpm/client": "workspace:*",
"@pnpm/create-cafs-store": "workspace:*",
"@pnpm/package-requester": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/normalize-path": "^3.0.0",
"@types/ramda": "0.28.20",

View File

@@ -38,7 +38,7 @@
"@pnpm/modules-yaml": "workspace:*",
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/proxyquire": "^1.3.28",
"@types/ramda": "0.28.20",

View File

@@ -24,6 +24,7 @@ import {
} from '@pnpm/types'
import promiseShare from 'promise-share'
import difference from 'ramda/src/difference'
import zipWith from 'ramda/src/zipWith'
import { getWantedDependencies, type WantedDependency } from './getWantedDependencies'
import { depPathToRef } from './depPathToRef'
import { createNodeIdForLinkedLocalPkg, type UpdateMatchingFunction } from './resolveDependencies'
@@ -173,7 +174,7 @@ export async function resolveDependencies (
)
}
const topParents: Array<{ name: string, version: string, linkedDir?: string }> = project.manifest
const topParents: Array<{ name: string, version: string, alias?: string, linkedDir?: string }> = project.manifest
? await getTopParents(
difference(
Object.keys(getAllDependenciesFromManifest(project.manifest)),
@@ -440,13 +441,17 @@ function getAliasToDependencyTypeMap (manifest: ProjectManifest): Record<string,
return depTypesOfAliases
}
async function getTopParents (pkgNames: string[], modules: string) {
async function getTopParents (pkgAliases: string[], modulesDir: string) {
const pkgs = await Promise.all(
pkgNames.map((pkgName) => path.join(modules, pkgName)).map(safeReadPackageJsonFromDir)
pkgAliases.map((alias) => path.join(modulesDir, alias)).map(safeReadPackageJsonFromDir)
)
return (
pkgs
.filter(Boolean) as DependencyManifest[]
)
.map(({ name, version }: DependencyManifest) => ({ name, version }))
return zipWith((manifest, alias) => {
if (!manifest) return null
return {
alias,
name: manifest.name,
version: manifest.version,
}
}, pkgs, pkgAliases)
.filter(Boolean) as DependencyManifest[]
}

View File

@@ -1,13 +1,13 @@
import filenamify from 'filenamify'
import path from 'path'
import semver from 'semver'
import { semverUtils } from '@yarnpkg/core'
import {
type Dependencies,
type PeerDependencyIssues,
type PeerDependencyIssuesByProjects,
import type {
Dependencies,
PeerDependencyIssues,
PeerDependencyIssuesByProjects,
} from '@pnpm/types'
import { depPathToFilename, createPeersFolderSuffixNewFormat } from '@pnpm/dependency-path'
import { type KeyValuePair } from 'ramda'
import isEmpty from 'ramda/src/isEmpty'
import mapValues from 'ramda/src/map'
import pick from 'ramda/src/pick'
@@ -51,7 +51,7 @@ export interface ProjectToResolve {
directNodeIdsByAlias: { [alias: string]: string }
// only the top dependencies that were already installed
// to avoid warnings about unresolved peer dependencies
topParents: Array<{ name: string, version: string }>
topParents: Array<{ name: string, version: string, alias?: string }>
rootDir: string // is only needed for logging
id: string
}
@@ -182,30 +182,32 @@ function createPkgsByName<T extends PartialResolvedPackage> (
dependenciesTree: DependenciesTree<T>,
{ directNodeIdsByAlias, topParents }: {
directNodeIdsByAlias: { [alias: string]: string }
topParents: Array<{ name: string, version: string, linkedDir?: string }>
topParents: Array<{ name: string, version: string, alias?: string, linkedDir?: string }>
}
) {
return Object.assign(
Object.fromEntries(
topParents.map(({ name, version, linkedDir }): KeyValuePair<string, ParentRef> => [
name,
{
depth: 0,
version,
nodeId: linkedDir,
},
])
),
toPkgByName(
Object
.keys(directNodeIdsByAlias)
.map((alias) => ({
alias,
node: dependenciesTree[directNodeIdsByAlias[alias]],
nodeId: directNodeIdsByAlias[alias],
}))
)
const parentRefs = toPkgByName(
Object
.keys(directNodeIdsByAlias)
.map((alias) => ({
alias,
node: dependenciesTree[directNodeIdsByAlias[alias]],
nodeId: directNodeIdsByAlias[alias],
}))
)
const _updateParentRefs = updateParentRefs.bind(null, parentRefs)
for (const { name, version, alias, linkedDir } of topParents) {
const pkg = {
alias,
depth: 0,
version,
nodeId: linkedDir,
}
_updateParentRefs(name, pkg)
if (alias && alias !== name) {
_updateParentRefs(alias, pkg)
}
}
return parentRefs
}
interface PeersCacheItem {
@@ -272,10 +274,10 @@ function resolvePeersOfNode<T extends PartialResolvedPackage> (
.every(([name, cachedNodeId]) => {
const parentPkgNodeId = parentPkgs[name]?.nodeId
if (!parentPkgNodeId || !cachedNodeId) return false
if (parentPkgs[name].nodeId === cachedNodeId) return true
if (parentPkgNodeId === cachedNodeId) return true
if (
ctx.pathsByNodeId[cachedNodeId] &&
ctx.pathsByNodeId[cachedNodeId] === ctx.pathsByNodeId[parentPkgs[name].nodeId!]
ctx.pathsByNodeId[cachedNodeId] === ctx.pathsByNodeId[parentPkgNodeId]
) return true
const parentDepPath = (ctx.dependenciesTree[parentPkgNodeId].resolvedPackage as T).depPath
if (!ctx.purePkgs.has(parentDepPath)) return false
@@ -572,16 +574,34 @@ interface ParentRef {
depth: number
// this is null only for already installed top dependencies
nodeId?: string
alias?: string
}
function toPkgByName<T extends PartialResolvedPackage> (nodes: Array<{ alias: string, nodeId: string, node: DependenciesTreeNode<T> }>): ParentRefs {
const pkgsByName: ParentRefs = {}
const _updateParentRefs = updateParentRefs.bind(null, pkgsByName)
for (const { alias, node, nodeId } of nodes) {
pkgsByName[alias] = {
const pkg = {
alias,
depth: node.depth,
nodeId,
version: node.resolvedPackage.version,
}
_updateParentRefs(alias, pkg)
if (alias !== node.resolvedPackage.name) {
_updateParentRefs(node.resolvedPackage.name, pkg)
}
}
return pkgsByName
}
function updateParentRefs (parentRefs: ParentRefs, newAlias: string, pkg: ParentRef) {
const existing = parentRefs[newAlias]
if (existing) {
const existingHasAlias = existing.alias != null || existing.alias !== newAlias
if (!existingHasAlias) return
const newHasAlias = pkg.alias != null || pkg.alias !== newAlias
if (newHasAlias && semver.gte(existing.version, pkg.version)) return
}
parentRefs[newAlias] = pkg
}

View File

@@ -516,3 +516,99 @@ describe('unmet peer dependency issue resolved from subdependency', () => {
expect(peerDependencyIssuesByProjects.project.bad.dep[0].resolvedFrom).toStrictEqual([{ name: 'foo', version: '1.0.0' }])
})
})
test('resolve peer dependencies with npm aliases', () => {
const fooPkg = {
name: 'foo',
depPath: 'foo/1.0.0',
version: '1.0.0',
peerDependencies: {
bar: '1.0.0',
},
}
const fooAliasPkg = {
name: 'foo',
depPath: 'foo/2.0.0',
version: '2.0.0',
peerDependencies: {
bar: '2.0.0',
},
}
const barPkg = {
name: 'bar',
depPath: 'bar/1.0.0',
version: '1.0.0',
peerDependencies: {},
}
const barAliasPkg = {
name: 'bar',
depPath: 'bar/2.0.0',
version: '2.0.0',
peerDependencies: {},
}
const { dependenciesGraph } = resolvePeers({
projects: [
{
directNodeIdsByAlias: {
foo: '>foo/1.0.0>',
bar: '>bar/1.0.0>',
'foo-next': '>foo/2.0.0>',
'bar-next': '>bar/2.0.0>',
},
topParents: [],
rootDir: '',
id: '',
},
],
dependenciesTree: {
'>foo/1.0.0>': {
children: {
bar: '>foo/1.0.0>bar/1.0.0>',
},
installable: true,
resolvedPackage: fooPkg,
depth: 0,
},
'>foo/1.0.0>bar/1.0.0>': {
children: {},
installable: true,
resolvedPackage: barPkg,
depth: 1,
},
'>foo/2.0.0>': {
children: {
bar: '>foo/2.0.0>bar/2.0.0>',
},
installable: true,
resolvedPackage: fooAliasPkg,
depth: 0,
},
'>foo/2.0.0>bar/2.0.0>': {
children: {},
installable: true,
resolvedPackage: barAliasPkg,
depth: 1,
},
'>bar/1.0.0>': {
children: {},
installable: true,
resolvedPackage: barPkg,
depth: 0,
},
'>bar/2.0.0>': {
children: {},
installable: true,
resolvedPackage: barAliasPkg,
depth: 0,
},
},
virtualStoreDir: '',
lockfileDir: '',
})
expect(Object.keys(dependenciesGraph)).toStrictEqual([
'bar/1.0.0',
'foo/1.0.0(bar@1.0.0)',
'bar/2.0.0',
'foo/2.0.0(bar@2.0.0)',
])
})

627
pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@
"@pnpm/prepare": "workspace:*",
"@pnpm/read-package-json": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/run-npm": "workspace:*",
"@pnpm/tabtab": "^0.1.2",
"@pnpm/test-fixtures": "workspace:*",

View File

@@ -42,7 +42,7 @@
"@pnpm/lockfile-types": "workspace:*",
"@pnpm/plugin-commands-deploy": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0"
"@pnpm/registry-mock": "3.7.0"
},
"dependencies": {
"@pnpm/cli-utils": "workspace:*",

View File

@@ -38,7 +38,7 @@
"@pnpm/filter-workspace-packages": "workspace:*",
"@pnpm/plugin-commands-publishing": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@types/cross-spawn": "^6.0.2",
"@types/is-windows": "^1.0.0",
"@types/npm-packlist": "^3.0.0",

View File

@@ -38,7 +38,7 @@
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/plugin-commands-licenses": "workspace:*",
"@pnpm/read-package-json": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@types/ramda": "0.28.20",
"@types/wrap-ansi": "^8.0.1",
"@types/zkochan__table": "npm:@types/table@6.0.0",

View File

@@ -37,7 +37,7 @@
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/plugin-commands-listing": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@types/ramda": "0.28.20",
"execa": "npm:safe-execa@0.1.2",
"strip-ansi": "^6.0.1",

View File

@@ -38,7 +38,7 @@
"@pnpm/plugin-commands-installation": "workspace:*",
"@pnpm/plugin-commands-outdated": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@pnpm/test-fixtures": "workspace:*",
"@types/ramda": "0.28.20",
"@types/wrap-ansi": "^8.0.1",

View File

@@ -37,7 +37,7 @@
"@pnpm/lockfile-file": "workspace:*",
"@pnpm/plugin-commands-store": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "3.5.0",
"@pnpm/registry-mock": "3.7.0",
"@types/archy": "0.0.32",
"@types/ramda": "0.28.20",
"@types/ssri": "^7.1.1",