fix(manifest-utils): normalize peer specs for protocol deps (#10442)

close #10417
This commit is contained in:
Lindsay Glenn
2026-01-17 15:44:51 +02:00
committed by Zoltan Kochan
parent 15e83b35a2
commit d58bdaf73f
9 changed files with 222 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/manifest-utils": patch
"@pnpm/resolve-dependencies": patch
pnpm: patch
---
Fix `--save-peer` to write valid semver ranges to `peerDependencies` for protocol-based installs (e.g. `jsr:`) by deriving from resolved versions when available and falling back to `*` if none is available [#10417](https://github.com/pnpm/pnpm/issues/10417).

1
.eslintcache Normal file
View File

File diff suppressed because one or more lines are too long

View File

@@ -61,6 +61,24 @@ test('pnpm add jsr:@<scope>/<name>', async () => {
} as Partial<LockfileFile>)
})
test('pnpm add jsr:@<scope>/<name> --save-peer writes a valid peer range', async () => {
prepare()
await add.handler({
...createOptions(),
savePeer: true,
}, ['jsr:@pnpm-e2e/foo'])
expect(loadJsonFileSync('package.json')).toMatchObject({
devDependencies: {
'@pnpm-e2e/foo': 'jsr:^0.1.0',
},
peerDependencies: {
'@pnpm-e2e/foo': '^0.1.0',
},
} as ProjectManifest)
})
test('pnpm add jsr:@<scope>/<name>@latest', async () => {
const project = prepare({
name: 'test-add-jsr',

View File

@@ -26,6 +26,8 @@ export async function updateProjectManifest (
nodeExecPath: wantedDep.nodeExecPath,
peer: importer.peer,
bareSpecifier: rdd.catalogLookup?.userSpecifiedBareSpecifier ?? rdd.normalizedBareSpecifier ?? wantedDep.bareSpecifier,
resolvedVersion: rdd.version,
pinnedVersion: importer.pinnedVersion,
saveType: importer.targetDependenciesField,
}
})

View File

@@ -34,14 +34,17 @@
"dependencies": {
"@pnpm/core-loggers": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/types": "workspace:*"
"@pnpm/semver.peer-range": "workspace:*",
"@pnpm/types": "workspace:*",
"semver": "catalog:"
},
"peerDependencies": {
"@pnpm/logger": "catalog:"
},
"devDependencies": {
"@pnpm/logger": "workspace:*",
"@pnpm/manifest-utils": "workspace:*"
"@pnpm/manifest-utils": "workspace:*",
"@types/semver": "catalog:"
},
"engines": {
"node": ">=18.12"

View File

@@ -1,9 +1,12 @@
import { packageManifestLogger } from '@pnpm/core-loggers'
import { isValidPeerRange } from '@pnpm/semver.peer-range'
import semver from 'semver'
import {
type DependenciesOrPeersField,
type DependenciesField,
DEPENDENCIES_FIELDS,
DEPENDENCIES_OR_PEER_FIELDS,
type PinnedVersion,
type ProjectManifest,
} from '@pnpm/types'
@@ -12,9 +15,36 @@ export interface PackageSpecObject {
nodeExecPath?: string
peer?: boolean
bareSpecifier?: string
resolvedVersion?: string
pinnedVersion?: PinnedVersion
saveType?: DependenciesField
}
function getPeerSpecifier (spec: string, resolvedVersion?: string, pinnedVersion?: PinnedVersion): string {
if (isValidPeerRange(spec)) return spec
const rangeFromResolved = resolvedVersion ? createVersionSpecFromResolvedVersion(resolvedVersion, pinnedVersion) : null
return rangeFromResolved ?? '*'
}
function createVersionSpecFromResolvedVersion (resolvedVersion: string, pinnedVersion?: PinnedVersion): string | null {
const parsed = semver.parse(resolvedVersion)
if (!parsed) return null
if (parsed.prerelease.length) return resolvedVersion
switch (pinnedVersion ?? 'major') {
case 'none':
case 'major':
return `^${resolvedVersion}`
case 'minor':
return `~${resolvedVersion}`
case 'patch':
return resolvedVersion
default:
return `^${resolvedVersion}`
}
}
export async function updateProjectManifestObject (
prefix: string,
packageManifest: ProjectManifest,
@@ -33,7 +63,11 @@ export async function updateProjectManifestObject (
}
if (packageSpec.peer === true) {
packageManifest.peerDependencies = packageManifest.peerDependencies ?? {}
packageManifest.peerDependencies[packageSpec.alias] = spec
packageManifest.peerDependencies[packageSpec.alias] = getPeerSpecifier(
spec,
packageSpec.resolvedVersion,
packageSpec.pinnedVersion
)
}
}
} else if (packageSpec.bareSpecifier) {

View File

@@ -1,4 +1,4 @@
import { guessDependencyType } from '@pnpm/manifest-utils'
import { guessDependencyType, updateProjectManifestObject } from '@pnpm/manifest-utils'
test('guessDependencyType()', () => {
expect(
@@ -23,3 +23,144 @@ test('guessDependencyType()', () => {
})
).toEqual('dependencies')
})
test('peer dependencies fall back to "*" when resolved version is unavailable (git)', async () => {
const manifest = await updateProjectManifestObject('/project', {}, [
{
alias: 'foo',
bareSpecifier: 'https://github.com/kevva/is-negative',
peer: true,
saveType: 'devDependencies',
},
])
expect(manifest.devDependencies).toStrictEqual({
foo: 'https://github.com/kevva/is-negative',
})
expect(manifest.peerDependencies).toStrictEqual({
foo: '*',
})
})
test('peer dependencies fall back to "*" when resolved version is unavailable (tarball)', async () => {
const manifest = await updateProjectManifestObject('/project', {}, [
{
alias: 'foo',
bareSpecifier: 'https://github.com/hegemonic/taffydb/tarball/master',
peer: true,
saveType: 'devDependencies',
},
])
expect(manifest.devDependencies).toStrictEqual({
foo: 'https://github.com/hegemonic/taffydb/tarball/master',
})
expect(manifest.peerDependencies).toStrictEqual({
foo: '*',
})
})
test('peer dependencies use derived range when resolved version is available (git)', async () => {
const manifest = await updateProjectManifestObject('/project', {}, [
{
alias: 'foo',
bareSpecifier: 'https://github.com/kevva/is-negative',
resolvedVersion: '2.1.0',
peer: true,
saveType: 'devDependencies',
},
])
expect(manifest.devDependencies).toStrictEqual({
foo: 'https://github.com/kevva/is-negative',
})
expect(manifest.peerDependencies).toStrictEqual({
foo: '^2.1.0',
})
})
test('peer dependencies honor pinned version when resolved version is available (tarball)', async () => {
const manifest = await updateProjectManifestObject('/project', {}, [
{
alias: 'foo',
bareSpecifier: 'https://github.com/hegemonic/taffydb/tarball/master',
resolvedVersion: '1.4.0',
pinnedVersion: 'minor',
peer: true,
saveType: 'devDependencies',
},
])
expect(manifest.devDependencies).toStrictEqual({
foo: 'https://github.com/hegemonic/taffydb/tarball/master',
})
expect(manifest.peerDependencies).toStrictEqual({
foo: '~1.4.0',
})
})
test('peer dependencies derive range from resolved version for jsr protocol', async () => {
const manifest = await updateProjectManifestObject('/project', {}, [
{
alias: 'foo',
bareSpecifier: 'jsr:^0.1.0',
resolvedVersion: '0.1.0',
peer: true,
saveType: 'devDependencies',
},
])
expect(manifest.devDependencies).toStrictEqual({
foo: 'jsr:^0.1.0',
})
expect(manifest.peerDependencies).toStrictEqual({
foo: '^0.1.0',
})
})
test('peer dependencies keep prerelease resolved version without prefix', async () => {
const manifest = await updateProjectManifestObject('/project', {}, [
{
alias: 'foo',
bareSpecifier: 'https://github.com/kevva/is-negative',
resolvedVersion: '2.1.0-rc.1',
pinnedVersion: 'minor',
peer: true,
saveType: 'devDependencies',
},
])
expect(manifest.devDependencies).toStrictEqual({
foo: 'https://github.com/kevva/is-negative',
})
expect(manifest.peerDependencies).toStrictEqual({
foo: '2.1.0-rc.1',
})
})
test('peer dependencies respect pinned version "patch" and "none"', async () => {
const cases = [
{ pinnedVersion: 'patch' as const, expected: '3.2.1' },
{ pinnedVersion: 'none' as const, expected: '^3.2.1' },
]
await Promise.all(cases.map(async ({ pinnedVersion, expected }) => {
const manifest = await updateProjectManifestObject('/project', {}, [
{
alias: 'foo',
bareSpecifier: 'https://github.com/kevva/is-negative',
resolvedVersion: '3.2.1',
pinnedVersion,
peer: true,
saveType: 'devDependencies',
},
])
expect(manifest.devDependencies).toStrictEqual({
foo: 'https://github.com/kevva/is-negative',
})
expect(manifest.peerDependencies).toStrictEqual({
foo: expected,
})
}))
})

View File

@@ -20,6 +20,9 @@
},
{
"path": "../../packages/types"
},
{
"path": "../../semver/peer-range"
}
]
}

9
pnpm-lock.yaml generated
View File

@@ -6286,9 +6286,15 @@ importers:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/semver.peer-range':
specifier: workspace:*
version: link:../../semver/peer-range
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
semver:
specifier: 'catalog:'
version: 7.7.2
devDependencies:
'@pnpm/logger':
specifier: workspace:*
@@ -6296,6 +6302,9 @@ importers:
'@pnpm/manifest-utils':
specifier: workspace:*
version: 'link:'
'@types/semver':
specifier: 'catalog:'
version: 7.5.3
pkg-manifest/read-package-json:
dependencies: