mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-29 18:35:18 -04:00
fix: preserve catalog version range policy on update (#12416)
A named catalog whose name parses as a version (e.g. catalog:express4-21) had its range policy overridden by pnpm update because whichVersionIsPinned misread the catalog: reference in the previous specifier as a pinned version. The catalog reference carries no pinning of its own, so the prefix from the catalog entry is now preserved. Closes https://github.com/pnpm/pnpm/issues/10321
This commit is contained in:
6
.changeset/catalog-update-policy.md
Normal file
6
.changeset/catalog-update-policy.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/resolving.npm-resolver": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed `pnpm update` overriding the version range policy of a named catalog whose name parses as a version (e.g. `catalog:express4-21`). The `catalog:` reference carries no pinning of its own, so the prefix from the catalog entry (such as `~`) is now preserved instead of being widened to `^` [#10321](https://github.com/pnpm/pnpm/issues/10321).
|
||||
@@ -2042,6 +2042,55 @@ describe('update', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// A named catalog whose name parses as a version (e.g. "express4-21") must not
|
||||
// have its update policy overridden. The "catalog:express4-21" reference in the
|
||||
// manifest carries no pinning of its own, so the "~" prefix from the catalog
|
||||
// entry must be preserved instead of being widened to "^" (issue #10321).
|
||||
test('update via install mutation preserves the ~ range of a version-like named catalog (issue #10321)', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:foo1-0',
|
||||
},
|
||||
}])
|
||||
|
||||
const mutateOpts = {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
'foo1-0': { '@pnpm.e2e/foo': '~1.0.0' },
|
||||
},
|
||||
}
|
||||
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
expect(readLockfile().catalogs['foo1-0']).toEqual({
|
||||
'@pnpm.e2e/foo': { specifier: '~1.0.0', version: '1.0.0' },
|
||||
})
|
||||
|
||||
// Simulate `pnpm update` via the "install" mutation with update=true.
|
||||
const { updatedCatalogs } = await mutateModules(
|
||||
installProjects(projects).map((project) => ({
|
||||
...project,
|
||||
mutation: 'install' as const,
|
||||
update: true,
|
||||
updatePackageManifest: true,
|
||||
})),
|
||||
mutateOpts
|
||||
)
|
||||
|
||||
// The "~" prefix must be preserved, not widened to "^".
|
||||
expect(updatedCatalogs).toEqual({
|
||||
'foo1-0': {
|
||||
'@pnpm.e2e/foo': '~1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(readLockfile().catalogs['foo1-0']).toEqual({
|
||||
'@pnpm.e2e/foo': { specifier: '~1.0.0', version: '1.0.0' },
|
||||
})
|
||||
})
|
||||
|
||||
// Similar to above but with updateToLatest (simulating `pnpm upgrade -r --latest`)
|
||||
test('update via install mutation with updateToLatest preserves catalog: in manifest (issue #11658)', async () => {
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
@@ -2,6 +2,11 @@ import type { PinnedVersion } from '@pnpm/types'
|
||||
import { parseRange } from 'semver-utils'
|
||||
|
||||
export function whichVersionIsPinned (spec: string): PinnedVersion | undefined {
|
||||
// A catalog reference carries no version pinning of its own; the pinning is
|
||||
// defined by the catalog entry it points to. Bail out so a catalog name that
|
||||
// happens to look like a version (e.g. "catalog:express4-21") isn't misread
|
||||
// as a pinned version.
|
||||
if (spec.startsWith('catalog:')) return undefined
|
||||
const colonIndex = spec.indexOf(':')
|
||||
if (colonIndex !== -1) {
|
||||
spec = spec.substring(colonIndex + 1)
|
||||
|
||||
@@ -15,6 +15,11 @@ test.each([
|
||||
['npm:@pnpm.e2e/qar@100.0.0', 'patch'],
|
||||
['jsr:@foo/foo@1.0.0', 'patch'],
|
||||
['jsr:foo@^1.0.0', 'major'],
|
||||
['catalog:', undefined],
|
||||
['catalog:default', undefined],
|
||||
['catalog:foo', undefined],
|
||||
// A catalog name that parses as a version must not be treated as a pin.
|
||||
['catalog:express4-21', undefined],
|
||||
])('whichVersionIsPinned()', (spec, expectedResult) => {
|
||||
expect(whichVersionIsPinned(spec)).toEqual(expectedResult)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user