mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-23 06:02:50 -04:00
fix: preserve named catalog group during interactive upgrade --latest (#11567)
When upgrading a dependency that uses a named catalog (e.g. "catalog:foo"), the previous specifier's catalog name now takes priority over the global saveCatalogName option. This prevents the package.json from being rewritten to "catalog:" and the updated version from landing in the default catalog instead of the named one. Closes #10115 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
6
.changeset/fix-named-catalog-upgrade.md
Normal file
6
.changeset/fix-named-catalog-upgrade.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/installing.deps-installer": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fix `pnpm upgrade --interactive --latest -r` not respecting named catalog groups. Previously, upgrading a dependency using a named catalog (e.g. `"catalog:foo"`) would incorrectly rewrite `package.json` to `"catalog:"` and place the updated version in the default catalog instead of the named one [#10115](https://github.com/pnpm/pnpm/issues/10115).
|
||||
@@ -711,8 +711,9 @@ export async function mutateModules (
|
||||
})
|
||||
|
||||
if (opts.catalogMode !== 'manual') {
|
||||
const catalogBareSpecifier = `catalog:${opts.saveCatalogName == null || opts.saveCatalogName === 'default' ? '' : opts.saveCatalogName}`
|
||||
for (const wantedDep of wantedDeps) {
|
||||
const perDepCatalogName = getPerDepCatalogName(wantedDep, opts.saveCatalogName)
|
||||
const catalogBareSpecifier = `catalog:${perDepCatalogName === 'default' ? '' : perDepCatalogName}`
|
||||
const catalog = resolveFromCatalog(opts.catalogs, { ...wantedDep, bareSpecifier: catalogBareSpecifier })
|
||||
const catalogDepSpecifier = matchCatalogResolveResult(catalog, pickCatalogSpecifier)
|
||||
|
||||
@@ -723,7 +724,7 @@ export async function mutateModules (
|
||||
semver.validRange(catalogDepSpecifier) &&
|
||||
semver.eq(wantedDep.bareSpecifier, catalogDepSpecifier)
|
||||
) {
|
||||
wantedDep.saveCatalogName = opts.saveCatalogName ?? 'default'
|
||||
wantedDep.saveCatalogName = perDepCatalogName
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1097,6 +1098,27 @@ function isWantedDepBareSpecifierSame (
|
||||
return prevCatalogEntrySpec === nextCatalogEntrySpec
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the catalog name for a dependency during installSome.
|
||||
*
|
||||
* If the dependency's previous specifier already uses a named catalog
|
||||
* (e.g. "catalog:foo"), that catalog name takes priority over the global
|
||||
* saveCatalogName option. This ensures that interactive updates and
|
||||
* `--latest` upgrades preserve the per-dependency catalog group.
|
||||
*/
|
||||
function getPerDepCatalogName (
|
||||
wantedDep: { prevSpecifier?: string },
|
||||
globalSaveCatalogName: string | undefined
|
||||
): string {
|
||||
if (wantedDep.prevSpecifier) {
|
||||
const catalogFromPrev = parseCatalogProtocol(wantedDep.prevSpecifier)
|
||||
if (catalogFromPrev != null) {
|
||||
return catalogFromPrev
|
||||
}
|
||||
}
|
||||
return globalSaveCatalogName ?? 'default'
|
||||
}
|
||||
|
||||
export async function addDependenciesToPackage (
|
||||
manifest: ProjectManifest,
|
||||
dependencySelectors: string[],
|
||||
|
||||
@@ -1755,6 +1755,119 @@ describe('update', () => {
|
||||
expect(Object.keys(readLockfile().snapshots)).toEqual(['@pnpm.e2e/foo@100.1.0'])
|
||||
})
|
||||
|
||||
test('update --latest works on named catalog dependency', async () => {
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:foo',
|
||||
},
|
||||
}])
|
||||
|
||||
const catalogs = {
|
||||
foo: { '@pnpm.e2e/foo': '1.0.0' },
|
||||
}
|
||||
|
||||
const mutateOpts = {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs,
|
||||
}
|
||||
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
expect(readLockfile().catalogs.foo).toEqual({
|
||||
'@pnpm.e2e/foo': { specifier: '1.0.0', version: '1.0.0' },
|
||||
})
|
||||
|
||||
const { updatedCatalogs, updatedManifest } = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['@pnpm.e2e/foo'],
|
||||
{
|
||||
...mutateOpts,
|
||||
dir: path.join(process.cwd(), 'project1'),
|
||||
allowNew: false,
|
||||
update: true,
|
||||
updateToLatest: true,
|
||||
})
|
||||
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:foo',
|
||||
},
|
||||
})
|
||||
expect(updatedCatalogs).toEqual({
|
||||
foo: {
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.catalogs).toEqual({
|
||||
foo: { '@pnpm.e2e/foo': { specifier: '100.1.0', version: '100.1.0' } },
|
||||
})
|
||||
expect(Object.keys(lockfile.snapshots)).toEqual(['@pnpm.e2e/foo@100.1.0'])
|
||||
})
|
||||
|
||||
test('update --latest works on named catalog dependency with catalogMode=prefer', async () => {
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:foo',
|
||||
},
|
||||
}])
|
||||
|
||||
const catalogs = {
|
||||
foo: { '@pnpm.e2e/foo': '1.0.0' },
|
||||
}
|
||||
|
||||
const mutateOpts = {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs,
|
||||
}
|
||||
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
expect(readLockfile().catalogs.foo).toEqual({
|
||||
'@pnpm.e2e/foo': { specifier: '1.0.0', version: '1.0.0' },
|
||||
})
|
||||
|
||||
const { updatedCatalogs, updatedManifest } = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['@pnpm.e2e/foo'],
|
||||
{
|
||||
...mutateOpts,
|
||||
catalogMode: 'prefer',
|
||||
dir: path.join(process.cwd(), 'project1'),
|
||||
allowNew: false,
|
||||
update: true,
|
||||
updateToLatest: true,
|
||||
})
|
||||
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:foo',
|
||||
},
|
||||
})
|
||||
expect(updatedCatalogs).toEqual({
|
||||
foo: {
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.catalogs).toEqual({
|
||||
foo: { '@pnpm.e2e/foo': { specifier: '100.1.0', version: '100.1.0' } },
|
||||
})
|
||||
expect(Object.keys(lockfile.snapshots)).toEqual(['@pnpm.e2e/foo@100.1.0'])
|
||||
})
|
||||
|
||||
// This test will update @pnpm.e2e/bar, but make sure @pnpm.e2e/foo is
|
||||
// untouched. On the registry-mock, the versions for @pnpm.e2e/bar are:
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user