mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-28 02:53:15 -04:00
fix: resolve peer dependencies from the root of the workspace (#3549)
This commit is contained in:
5
.changeset/lucky-walls-reflect.md
Normal file
5
.changeset/lucky-walls-reflect.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/resolve-dependencies": patch
|
||||
---
|
||||
|
||||
Peer dependencies should get resolved from the workspace root.
|
||||
@@ -44,7 +44,6 @@
|
||||
"dependency-path": "workspace:8.0.1",
|
||||
"encode-registry": "^3.0.0",
|
||||
"get-npm-tarball-url": "^2.0.2",
|
||||
"import-from": "^3.0.0",
|
||||
"path-exists": "^4.0.0",
|
||||
"ramda": "^0.27.1",
|
||||
"replace-string": "^3.1.0",
|
||||
|
||||
@@ -4,7 +4,6 @@ import PnpmError from '@pnpm/error'
|
||||
import logger from '@pnpm/logger'
|
||||
import { Dependencies } from '@pnpm/types'
|
||||
import { depPathToFilename } from 'dependency-path'
|
||||
import importFrom from 'import-from'
|
||||
import { KeyValuePair } from 'ramda'
|
||||
import fromPairs from 'ramda/src/fromPairs'
|
||||
import isEmpty from 'ramda/src/isEmpty'
|
||||
@@ -66,28 +65,15 @@ export default function<T extends PartialResolvedPackage> (
|
||||
} {
|
||||
const depGraph: GenericDependenciesGraph<T> = {}
|
||||
const pathsByNodeId = {}
|
||||
const _createPkgsByName = createPkgsByName.bind(null, opts.dependenciesTree)
|
||||
const rootProject = opts.projects.length > 1 ? opts.projects.find(({ id }) => id === '.') : null
|
||||
const rootPkgsByName = rootProject == null ? {} : _createPkgsByName(rootProject)
|
||||
|
||||
for (const { directNodeIdsByAlias, topParents, rootDir } of opts.projects) {
|
||||
const pkgsByName = Object.assign(
|
||||
fromPairs(
|
||||
topParents.map(({ name, version }: {name: string, version: string}): KeyValuePair<string, ParentRef> => [
|
||||
name,
|
||||
{
|
||||
depth: 0,
|
||||
version,
|
||||
},
|
||||
])
|
||||
),
|
||||
toPkgByName(
|
||||
Object
|
||||
.keys(directNodeIdsByAlias)
|
||||
.map((alias) => ({
|
||||
alias,
|
||||
node: opts.dependenciesTree[directNodeIdsByAlias[alias]],
|
||||
nodeId: directNodeIdsByAlias[alias],
|
||||
}))
|
||||
)
|
||||
)
|
||||
const pkgsByName = {
|
||||
...rootPkgsByName,
|
||||
..._createPkgsByName({ directNodeIdsByAlias, topParents }),
|
||||
}
|
||||
|
||||
resolvePeersOfChildren(directNodeIdsByAlias, pkgsByName, {
|
||||
dependenciesTree: opts.dependenciesTree,
|
||||
@@ -122,6 +108,35 @@ export default function<T extends PartialResolvedPackage> (
|
||||
}
|
||||
}
|
||||
|
||||
function createPkgsByName<T extends PartialResolvedPackage> (
|
||||
dependenciesTree: DependenciesTree<T>,
|
||||
{ directNodeIdsByAlias, topParents }: {
|
||||
directNodeIdsByAlias: {[alias: string]: string}
|
||||
topParents: Array<{name: string, version: string}>
|
||||
}
|
||||
) {
|
||||
return Object.assign(
|
||||
fromPairs(
|
||||
topParents.map(({ name, version }): KeyValuePair<string, ParentRef> => [
|
||||
name,
|
||||
{
|
||||
depth: 0,
|
||||
version,
|
||||
},
|
||||
])
|
||||
),
|
||||
toPkgByName(
|
||||
Object
|
||||
.keys(directNodeIdsByAlias)
|
||||
.map((alias) => ({
|
||||
alias,
|
||||
node: dependenciesTree[directNodeIdsByAlias[alias]],
|
||||
nodeId: directNodeIdsByAlias[alias],
|
||||
}))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
interface PeersCacheItem {
|
||||
depPath: string
|
||||
resolvedPeers: Array<[string, string]>
|
||||
@@ -367,34 +382,26 @@ function resolvePeers<T extends PartialResolvedPackage> (
|
||||
for (const peerName in ctx.resolvedPackage.peerDependencies) { // eslint-disable-line:forin
|
||||
const peerVersionRange = ctx.resolvedPackage.peerDependencies[peerName]
|
||||
|
||||
let resolved = ctx.parentPkgs[peerName]
|
||||
const resolved = ctx.parentPkgs[peerName]
|
||||
|
||||
if (!resolved) {
|
||||
missingPeers.push(peerName)
|
||||
try {
|
||||
const { version } = importFrom(ctx.rootDir, `${peerName}/package.json`) as { version: string }
|
||||
resolved = {
|
||||
depth: -1,
|
||||
version,
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
ctx.resolvedPackage.peerDependenciesMeta?.[peerName]?.optional === true
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const friendlyPath = nodeIdToFriendlyPath(ctx.nodeId, ctx.dependenciesTree)
|
||||
const message = `${friendlyPath ? `${friendlyPath}: ` : ''}${packageFriendlyId(ctx.resolvedPackage)} \
|
||||
requires a peer of ${peerName}@${peerVersionRange} but none was installed.`
|
||||
if (ctx.strictPeerDependencies) {
|
||||
throw new PnpmError('MISSING_PEER_DEPENDENCY', message)
|
||||
}
|
||||
logger.warn({
|
||||
message,
|
||||
prefix: ctx.rootDir,
|
||||
})
|
||||
if (
|
||||
ctx.resolvedPackage.peerDependenciesMeta?.[peerName]?.optional === true
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const friendlyPath = nodeIdToFriendlyPath(ctx.nodeId, ctx.dependenciesTree)
|
||||
const message = `${friendlyPath ? `${friendlyPath}: ` : ''}${packageFriendlyId(ctx.resolvedPackage)} \
|
||||
requires a peer of ${peerName}@${peerVersionRange} but none was installed.`
|
||||
if (ctx.strictPeerDependencies) {
|
||||
throw new PnpmError('MISSING_PEER_DEPENDENCY', message)
|
||||
}
|
||||
logger.warn({
|
||||
message,
|
||||
prefix: ctx.rootDir,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (!semver.satisfies(resolved.version, peerVersionRange)) {
|
||||
|
||||
@@ -178,25 +178,53 @@ test('strict-peer-dependencies: error is thrown when cannot resolve peer depende
|
||||
).rejects.toThrow(/ajv-keywords@1.5.0 requires a peer of ajv@>=4.10.0 but none was installed./)
|
||||
})
|
||||
|
||||
test('warning is not reported if the peer dependency can be required from a node_modules of a parent directory', async () => {
|
||||
prepareEmpty()
|
||||
test('peer dependency is resolved from the dependencies of the workspace root project', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: { name: 'root' },
|
||||
},
|
||||
{
|
||||
location: 'pkg',
|
||||
package: {},
|
||||
},
|
||||
])
|
||||
const reporter = jest.fn()
|
||||
await mutateModules([
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest: {
|
||||
name: 'root',
|
||||
version: '1.0.0',
|
||||
|
||||
const manifest = await addDependenciesToPackage({}, ['ajv@4.10.0'], await testDefaults())
|
||||
dependencies: {
|
||||
ajv: '4.10.0',
|
||||
},
|
||||
},
|
||||
mutation: 'install',
|
||||
rootDir: process.cwd(),
|
||||
},
|
||||
{
|
||||
buildIndex: 0,
|
||||
manifest: {
|
||||
name: 'root',
|
||||
version: '1.0.0',
|
||||
|
||||
await fs.mkdir('pkg')
|
||||
dependencies: {
|
||||
'ajv-keywords': '1.5.0',
|
||||
},
|
||||
},
|
||||
mutation: 'install',
|
||||
rootDir: path.resolve('pkg'),
|
||||
},
|
||||
], await testDefaults({ reporter }))
|
||||
|
||||
process.chdir('pkg')
|
||||
|
||||
const reporter = sinon.spy()
|
||||
|
||||
await addDependenciesToPackage(manifest, ['ajv-keywords@1.5.0'], await testDefaults({ reporter }))
|
||||
|
||||
const logMatcher = sinon.match({
|
||||
expect(reporter).not.toHaveBeenCalledWith({
|
||||
message: 'ajv-keywords@1.5.0 requires a peer of ajv@>=4.10.0 but none was installed.',
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(0)
|
||||
const lockfile = await projects.root.readLockfile()
|
||||
expect(lockfile.importers.pkg?.dependencies?.['ajv-keywords']).toBe('1.5.0_ajv@4.10.0')
|
||||
})
|
||||
|
||||
test('warning is reported when cannot resolve peer dependency for non-top-level dependency', async () => {
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -2748,7 +2748,6 @@ importers:
|
||||
dependency-path: workspace:8.0.1
|
||||
encode-registry: ^3.0.0
|
||||
get-npm-tarball-url: ^2.0.2
|
||||
import-from: ^3.0.0
|
||||
path-exists: ^4.0.0
|
||||
ramda: ^0.27.1
|
||||
replace-string: ^3.1.0
|
||||
@@ -2772,7 +2771,6 @@ importers:
|
||||
dependency-path: link:../dependency-path
|
||||
encode-registry: 3.0.0
|
||||
get-npm-tarball-url: 2.0.2
|
||||
import-from: 3.0.0
|
||||
path-exists: 4.0.0
|
||||
ramda: 0.27.1
|
||||
replace-string: 3.1.0
|
||||
@@ -8811,13 +8809,6 @@ packages:
|
||||
resolve-from: 4.0.0
|
||||
dev: true
|
||||
|
||||
/import-from/3.0.0:
|
||||
resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
resolve-from: 5.0.0
|
||||
dev: false
|
||||
|
||||
/import-local/3.0.2:
|
||||
resolution: {integrity: sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
Reference in New Issue
Block a user