mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-28 02:53:15 -04:00
feat: support catalog: protocol on pnpm update (#9517)
This commit is contained in:
6
.changeset/fancy-files-wish.md
Normal file
6
.changeset/fancy-files-wish.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/resolve-dependencies": minor
|
||||
pnpm: minor
|
||||
---
|
||||
|
||||
The `pnpm update` command now supports updating `catalog:` protocol dependencies and writes new specifiers to `pnpm-workspace.yaml`.
|
||||
@@ -1325,11 +1325,6 @@ describe('add', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// The 'pnpm update' command should eventually support updates of dependencies
|
||||
// in the catalog. This is a more involved feature since pnpm-workspace.yaml
|
||||
// needs to be edited. Until the catalog update feature is implemented, ensure
|
||||
// pnpm update does not touch or rewrite dependencies using the catalog
|
||||
// protocol.
|
||||
describe('update', () => {
|
||||
// Many of the update tests use @pnpm.e2e/foo, which has the following
|
||||
// versions currently published to the https://github.com/pnpm/registry-mock
|
||||
@@ -1346,38 +1341,7 @@ describe('update', () => {
|
||||
// is-positive since public packages can release new versions and break the
|
||||
// tests here.
|
||||
|
||||
test('update does not modify catalog: protocol', async () => {
|
||||
const { options, projects } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
}])
|
||||
|
||||
const { updatedManifest } = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['@pnpm.e2e/foo'],
|
||||
{
|
||||
...options,
|
||||
dir: path.join(options.lockfileDir, 'project1'),
|
||||
lockfileOnly: true,
|
||||
allowNew: false,
|
||||
update: true,
|
||||
catalogs: {
|
||||
default: { '@pnpm.e2e/foo': '^1.0.0' },
|
||||
},
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged.
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('update does not upgrade cataloged dependency', async () => {
|
||||
test('update works on cataloged dependency', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
@@ -1385,28 +1349,30 @@ describe('update', () => {
|
||||
},
|
||||
}])
|
||||
|
||||
const catalogs = {
|
||||
default: { '@pnpm.e2e/foo': '1.0.0' },
|
||||
}
|
||||
const mutateOpts = {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs,
|
||||
// Start by using 1.0.0 as the specifier. We'll then change this to ^1.0.0
|
||||
// and to test pnpm properly updates from 1.0.0 to 1.3.0.
|
||||
catalogs: {
|
||||
default: { '@pnpm.e2e/foo': '1.0.0' },
|
||||
},
|
||||
}
|
||||
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
// Updating the catalog from 1.0.0 to ^1.0.0. This should still lock to the
|
||||
// existing 1.0.0 version despite version 1.3.0 existing.
|
||||
catalogs.default['@pnpm.e2e/foo'] = '^1.0.0'
|
||||
// Changing the catalog from 1.0.0 to ^1.0.0. This should still lock to the
|
||||
// existing 1.0.0 version despite version 1.3.0 available on the registry.
|
||||
mutateOpts.catalogs.default['@pnpm.e2e/foo'] = '^1.0.0'
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
// Sanity check that the @pnpm.e2e/foo dependency is installed on the older
|
||||
// requested version.
|
||||
expect(readLockfile().catalogs.default).toEqual({
|
||||
'@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged after running an update.
|
||||
const { updatedManifest } = await addDependenciesToPackage(
|
||||
const { updatedCatalogs, updatedManifest } = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['@pnpm.e2e/foo'],
|
||||
{
|
||||
@@ -1415,21 +1381,99 @@ describe('update', () => {
|
||||
update: true,
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged after running an update. The
|
||||
// change should be reflected in the returned updatedCatalogs object
|
||||
// instead.
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
})
|
||||
|
||||
// The lockfile should only contain 1.0.0 and not 1.3.0 (or a later version).
|
||||
expect(readLockfile()).toMatchObject({
|
||||
catalogs: { default: { '@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' } } },
|
||||
packages: { '@pnpm.e2e/foo@1.0.0': expect.any(Object) },
|
||||
expect(updatedCatalogs).toEqual({
|
||||
default: {
|
||||
'@pnpm.e2e/foo': '^1.3.0',
|
||||
},
|
||||
})
|
||||
|
||||
// The lockfile should also contain the updated ^1.3.0 reference.
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.catalogs).toEqual({
|
||||
default: { '@pnpm.e2e/foo': { specifier: '^1.3.0', version: '1.3.0' } },
|
||||
})
|
||||
|
||||
// Ensure the old 1.0.0 version is no longer used.
|
||||
expect(Object.keys(lockfile.snapshots)).toEqual(['@pnpm.e2e/foo@1.3.0'])
|
||||
})
|
||||
|
||||
test('update latest does not modify catalog: protocol', async () => {
|
||||
test('update works on named catalog', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:foo',
|
||||
},
|
||||
}])
|
||||
|
||||
// Start by using 1.0.0 as the specifier. We'll then change this to ^1.0.0
|
||||
// and to test pnpm properly updates from 1.0.0 to 1.3.0.
|
||||
const mutateOpts = {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
foo: { '@pnpm.e2e/foo': '1.0.0' },
|
||||
},
|
||||
}
|
||||
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
// Changing the catalog from 1.0.0 to ^1.0.0. This should still lock to the
|
||||
// existing 1.0.0 version despite version 1.3.0 available on the registry.
|
||||
mutateOpts.catalogs.foo['@pnpm.e2e/foo'] = '^1.0.0'
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
// Sanity check that the @pnpm.e2e/foo dependency is installed on the older
|
||||
// requested version.
|
||||
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(options.lockfileDir, 'project1'),
|
||||
update: true,
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged after running an update. The
|
||||
// change should be reflected in the returned updatedCatalogs object
|
||||
// instead.
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:foo',
|
||||
},
|
||||
})
|
||||
expect(updatedCatalogs).toEqual({
|
||||
foo: {
|
||||
'@pnpm.e2e/foo': '^1.3.0',
|
||||
},
|
||||
})
|
||||
|
||||
// The lockfile should also contain the updated ^1.3.0 reference.
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.catalogs).toEqual({
|
||||
foo: { '@pnpm.e2e/foo': { specifier: '^1.3.0', version: '1.3.0' } },
|
||||
})
|
||||
|
||||
// Ensure the old 1.0.0 version is no longer used.
|
||||
expect(Object.keys(lockfile.snapshots)).toEqual(['@pnpm.e2e/foo@1.3.0'])
|
||||
})
|
||||
|
||||
test('update --latest works on cataloged dependency', async () => {
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
@@ -1455,7 +1499,7 @@ describe('update', () => {
|
||||
'@pnpm.e2e/foo': { specifier: '1.0.0', version: '1.0.0' },
|
||||
})
|
||||
|
||||
const { updatedManifest } = await addDependenciesToPackage(
|
||||
const { updatedCatalogs, updatedManifest } = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['@pnpm.e2e/foo'],
|
||||
{
|
||||
@@ -1466,15 +1510,111 @@ describe('update', () => {
|
||||
updateToLatest: true,
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged.
|
||||
// Expecting the manifest to remain unchanged after running an update. The
|
||||
// change should be reflected in the returned updatedCatalogs object
|
||||
// instead.
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
})
|
||||
expect(updatedCatalogs).toEqual({
|
||||
default: {
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(Object.keys(readLockfile().snapshots)).toEqual(['@pnpm.e2e/foo@1.0.0'])
|
||||
expect(Object.keys(readLockfile().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:
|
||||
//
|
||||
// - 100.0.0
|
||||
// - 100.1.0
|
||||
test('update only affects matching filter', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
'@pnpm.e2e/bar': 'catalog:',
|
||||
},
|
||||
}])
|
||||
|
||||
const mutateOpts = {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: {
|
||||
// Start by using exact versions for specifiers. We'll then change this to be a range below.
|
||||
'@pnpm.e2e/foo': '1.0.0',
|
||||
'@pnpm.e2e/bar': '100.0.0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
// Adding ^ to the catalog config entries. This allows the update process to
|
||||
// consider newer versions to update to for this test.
|
||||
mutateOpts.catalogs.default['@pnpm.e2e/foo'] = '^1.0.0'
|
||||
mutateOpts.catalogs.default['@pnpm.e2e/bar'] = '^100.0.0'
|
||||
await mutateModules(installProjects(projects), mutateOpts)
|
||||
|
||||
// Sanity check dependencies are still installed on older requested version
|
||||
// and not accidentally updated due to adding ^ above.
|
||||
expect(readLockfile().catalogs.default).toEqual({
|
||||
'@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
'@pnpm.e2e/bar': { specifier: '^100.0.0', version: '100.0.0' },
|
||||
})
|
||||
|
||||
const { updatedCatalogs, updatedManifest } = await addDependenciesToPackage(
|
||||
projects['project1' as ProjectId],
|
||||
['@pnpm.e2e/bar'],
|
||||
{
|
||||
...mutateOpts,
|
||||
dir: path.join(options.lockfileDir, 'project1'),
|
||||
update: true,
|
||||
updateMatching: (pkgName) => pkgName === '@pnpm.e2e/bar',
|
||||
})
|
||||
|
||||
// Expecting the manifest to remain unchanged after running an update. The
|
||||
// change should be reflected in the returned updatedCatalogs object
|
||||
// instead.
|
||||
expect(updatedManifest).toEqual({
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
'@pnpm.e2e/bar': 'catalog:',
|
||||
},
|
||||
})
|
||||
expect(updatedCatalogs).toEqual({
|
||||
default: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
// The lockfile should also contain the updated ^100.1.0 reference.
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile).toEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/foo': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
'@pnpm.e2e/bar': { specifier: '^100.1.0', version: '100.1.0' },
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/foo@1.0.0': expect.objectContaining({}),
|
||||
'@pnpm.e2e/bar@100.1.0': expect.objectContaining({}),
|
||||
},
|
||||
}))
|
||||
|
||||
// Ensure the old 1.0.0 version is no longer used.
|
||||
expect(Object.keys(lockfile.snapshots)).toEqual([
|
||||
'@pnpm.e2e/bar@100.1.0',
|
||||
'@pnpm.e2e/foo@1.0.0',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type CatalogSnapshots } from '@pnpm/lockfile.types'
|
||||
import { type ResolvedDirectDependency } from './resolveDependencyTree'
|
||||
|
||||
export function getCatalogSnapshots (resolvedDirectDeps: readonly ResolvedDirectDependency[]): CatalogSnapshots {
|
||||
export function getCatalogSnapshots (
|
||||
resolvedDirectDeps: readonly ResolvedDirectDependency[],
|
||||
updatedCatalogs?: Catalogs
|
||||
): CatalogSnapshots {
|
||||
const catalogSnapshots: CatalogSnapshots = {}
|
||||
const catalogedDeps = resolvedDirectDeps.filter(isCatalogedDep)
|
||||
|
||||
for (const dep of catalogedDeps) {
|
||||
const snapshotForSingleCatalog = (catalogSnapshots[dep.catalogLookup.catalogName] ??= {})
|
||||
const updatedSpecifier = updatedCatalogs?.[dep.catalogLookup.catalogName]?.[dep.alias]
|
||||
|
||||
snapshotForSingleCatalog[dep.alias] = {
|
||||
specifier: dep.catalogLookup.specifier,
|
||||
// The "updated specifier" will be present when pnpm add/update is ran and
|
||||
// bare specifiers need to be added in the pnpm-workspace.yaml file. When
|
||||
// this happens, the updated specifier should be saved to lockfile instead
|
||||
// of the original specifier before the update.
|
||||
specifier: updatedSpecifier ?? dep.catalogLookup.specifier,
|
||||
version: dep.version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,9 @@ export async function resolveDependencies (
|
||||
}
|
||||
}
|
||||
|
||||
newLockfile.catalogs = getCatalogSnapshots(Object.values(resolvedImporters).flatMap(({ directDependencies }) => directDependencies))
|
||||
newLockfile.catalogs = getCatalogSnapshots(
|
||||
Object.values(resolvedImporters).flatMap(({ directDependencies }) => directDependencies),
|
||||
updatedCatalogs)
|
||||
|
||||
// waiting till package requests are finished
|
||||
async function waitTillAllFetchingsFinish (): Promise<void> {
|
||||
|
||||
@@ -559,16 +559,8 @@ async function resolveDependenciesOfImporterDependency (
|
||||
// snapshot to ensure all projects using the same cataloged dependency get the
|
||||
// same version.
|
||||
if (catalogLookup != null) {
|
||||
const existingVersion = getCatalogExistingVersionFromSnapshot(catalogLookup, ctx.wantedLockfile, extendedWantedDep.wantedDependency)
|
||||
|
||||
// If there's an existing version, always use it to prevent "pnpm update"
|
||||
// from updating the catalog protocol. A future change will remove this
|
||||
// condition to support updating specifiers in pnpm-workspace.yaml
|
||||
// functionality.
|
||||
extendedWantedDep.wantedDependency.bareSpecifier = existingVersion != null
|
||||
? replaceVersionInBareSpecifier(catalogLookup.specifier, existingVersion)
|
||||
: catalogLookup.specifier
|
||||
extendedWantedDep.preferredVersion = existingVersion
|
||||
extendedWantedDep.wantedDependency.bareSpecifier = catalogLookup.specifier
|
||||
extendedWantedDep.preferredVersion = getCatalogExistingVersionFromSnapshot(catalogLookup, ctx.wantedLockfile, extendedWantedDep.wantedDependency)
|
||||
}
|
||||
|
||||
const result = await resolveDependenciesOfDependency(
|
||||
@@ -578,12 +570,6 @@ async function resolveDependenciesOfImporterDependency (
|
||||
...importer.options,
|
||||
parentPkgAliases: importer.parentPkgAliases,
|
||||
pickLowestVersion: pickLowestVersion && !importer.updatePackageManifest,
|
||||
// Cataloged dependencies cannot be upgraded yet since they require
|
||||
// updating the pnpm-workspace.yaml file. This will be handled in a future
|
||||
// version of pnpm.
|
||||
updateToLatest: catalogLookup != null
|
||||
? false
|
||||
: importer.options.updateToLatest,
|
||||
pinnedVersion: importer.pinnedVersion,
|
||||
},
|
||||
extendedWantedDep
|
||||
@@ -881,16 +867,8 @@ async function resolveDependenciesOfDependency (
|
||||
// as an importer separately, and we can rely on that process keeping the
|
||||
// importers lockfile catalog snapshots up to date.
|
||||
if (catalogLookup != null) {
|
||||
const existingVersion = getCatalogExistingVersionFromSnapshot(catalogLookup, ctx.wantedLockfile, extendedWantedDep.wantedDependency)
|
||||
|
||||
// If there's an existing version, always use it to prevent "pnpm update"
|
||||
// from updating the catalog protocol. A future change will remove this
|
||||
// condition to support updating specifiers in pnpm-workspace.yaml
|
||||
// functionality.
|
||||
extendedWantedDep.wantedDependency.bareSpecifier = existingVersion != null
|
||||
? replaceVersionInBareSpecifier(catalogLookup.specifier, existingVersion)
|
||||
: catalogLookup.specifier
|
||||
extendedWantedDep.preferredVersion = existingVersion
|
||||
extendedWantedDep.wantedDependency.bareSpecifier = catalogLookup.specifier
|
||||
extendedWantedDep.preferredVersion = getCatalogExistingVersionFromSnapshot(catalogLookup, ctx.wantedLockfile, extendedWantedDep.wantedDependency)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ export async function resolveDependencyTree<T> (
|
||||
if (existingCatalog != null) {
|
||||
if (existingCatalog !== normalizedBareSpecifier) {
|
||||
globalWarn(
|
||||
`Skip adding ${alias} to catalogs.${saveCatalogName} because it already exists as ${existingCatalog}`
|
||||
`Skip adding ${alias} to the default catalog because it already exists as ${existingCatalog}. Please use \`pnpm update\` to update the catalogs.`
|
||||
)
|
||||
}
|
||||
} else if (saveCatalogName != null && normalizedBareSpecifier != null && version != null) {
|
||||
|
||||
Reference in New Issue
Block a user