fix: ensure that recursive pnpm update --latest <pkg> updates only the specified package (#8933)

* test(pnpm): expand dedupePeers test to account for other dependencies in same package

Previously, this test only asserted that _other_ monorepo packages were unaffected,
but it did not check other dependencies of the _same_ monorepo package.

* fix: ensure that recursive update --latest only updates matched packages

* fix: move update check to resolveDependendency

* refactor: move updateToLatest conditional up in resolveDependency

* refactor: make update types mutually exclusive in resolveDependencies

* refactor: rename 'in-range' update type to 'compatible'

Co-authored-by: Zoltan Kochan <z@kochan.io>

* refactor: use update union type in package-requester and store-controller-type

* docs: add changesets

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
Fotis Papadogeorgopoulos
2025-01-07 03:08:26 +02:00
committed by GitHub
parent c5080ded56
commit dde650b96f
8 changed files with 32 additions and 14 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/resolve-dependencies": patch
---
Fix a case in `resolveDependencies`, whereby an importer that should not have been updated altogether, was being updated when `updateToLatest` was specified in the options.

View File

@@ -0,0 +1,8 @@
---
"@pnpm/package-requester": major
"@pnpm/store-controller-types": major
---
`RequestPackageOptions` now takes a union type for the `update` option, instead of a separate `updateToLatest` option.
This avoids pitfalls around specifying only `update` or, specifying `update: false`, but still providing `updateToLatest: true`.

View File

@@ -0,0 +1,5 @@
---
"pnpm": patch
---
Ensure that recursive `pnpm update --latest <pkg>` updates only the specified package, with `dedupe-peer-dependents=true`.

View File

@@ -187,7 +187,7 @@ async function resolveAndFetch (
projectDir: options.projectDir, projectDir: options.projectDir,
registry: options.registry, registry: options.registry,
workspacePackages: options.workspacePackages, workspacePackages: options.workspacePackages,
updateToLatest: options.updateToLatest, updateToLatest: options.update === 'latest',
injectWorkspacePackages: options.injectWorkspacePackages, injectWorkspacePackages: options.injectWorkspacePackages,
}), { priority: options.downloadPriority }) }), { priority: options.downloadPriority })

View File

@@ -15,7 +15,7 @@ import loadJsonFile from 'load-json-file'
import nock from 'nock' import nock from 'nock'
import normalize from 'normalize-path' import normalize from 'normalize-path'
import tempy from 'tempy' import tempy from 'tempy'
import { type PkgResolutionId, type PkgRequestFetchResult } from '@pnpm/store-controller-types' import { type PkgResolutionId, type PkgRequestFetchResult, type RequestPackageOptions } from '@pnpm/store-controller-types'
const registry = `http://localhost:${REGISTRY_MOCK_PORT}` const registry = `http://localhost:${REGISTRY_MOCK_PORT}`
const f = fixtures(__dirname) const f = fixtures(__dirname)
@@ -182,7 +182,7 @@ test('refetch local tarball if its integrity has changed', async () => {
registry, registry,
skipFetch: true, skipFetch: true,
update: false, update: false,
} } satisfies RequestPackageOptions
{ {
const requestPackage = createPackageRequester({ const requestPackage = createPackageRequester({
@@ -288,7 +288,7 @@ test('refetch local tarball if its integrity has changed. The requester does not
projectDir, projectDir,
registry, registry,
update: false, update: false,
} } satisfies RequestPackageOptions
{ {
const requestPackage = createPackageRequester({ const requestPackage = createPackageRequester({

View File

@@ -824,11 +824,10 @@ async function resolveDependenciesOfDependency (
prefix: options.prefix, prefix: options.prefix,
proceed: extendedWantedDep.proceed || updateShouldContinue || ctx.updatedSet.size > 0, proceed: extendedWantedDep.proceed || updateShouldContinue || ctx.updatedSet.size > 0,
publishedBy: options.publishedBy, publishedBy: options.publishedBy,
update, update: update ? options.updateToLatest ? 'latest' : 'compatible' : false,
updateDepth, updateDepth,
updateMatching: options.updateMatching, updateMatching: options.updateMatching,
supportedArchitectures: options.supportedArchitectures, supportedArchitectures: options.supportedArchitectures,
updateToLatest: options.updateToLatest,
parentIds: options.parentIds, parentIds: options.parentIds,
} }
const resolveDependencyResult = await resolveDependency(extendedWantedDep.wantedDependency, ctx, resolveDependencyOpts) const resolveDependencyResult = await resolveDependency(extendedWantedDep.wantedDependency, ctx, resolveDependencyOpts)
@@ -1175,11 +1174,10 @@ interface ResolveDependencyOptions {
proceed: boolean proceed: boolean
publishedBy?: Date publishedBy?: Date
pickLowestVersion?: boolean pickLowestVersion?: boolean
update: boolean update: false | 'compatible' | 'latest'
updateDepth: number updateDepth: number
updateMatching?: UpdateMatchingFunction updateMatching?: UpdateMatchingFunction
supportedArchitectures?: SupportedArchitectures supportedArchitectures?: SupportedArchitectures
updateToLatest?: boolean
} }
type ResolveDependencyResult = PkgAddress | LinkedDependency | null type ResolveDependencyResult = PkgAddress | LinkedDependency | null
@@ -1260,7 +1258,6 @@ async function resolveDependency (
err.pkgsStack = getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById) err.pkgsStack = getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById)
return err return err
}, },
updateToLatest: options.updateToLatest,
injectWorkspacePackages: ctx.injectWorkspacePackages, injectWorkspacePackages: ctx.injectWorkspacePackages,
}) })
} catch (err: any) { // eslint-disable-line } catch (err: any) { // eslint-disable-line

View File

@@ -115,6 +115,7 @@ test('partial update --latest in a workspace should not affect other packages wh
dependencies: { dependencies: {
'@pnpm.e2e/foo': '1.0.0', '@pnpm.e2e/foo': '1.0.0',
'@pnpm.e2e/bar': '100.0.0',
}, },
}, },
}, },
@@ -128,19 +129,22 @@ auto-install-peers=false`, 'utf8')
await addDistTag({ package: '@pnpm.e2e/foo', version: '2.0.0', distTag: 'latest' }) await addDistTag({ package: '@pnpm.e2e/foo', version: '2.0.0', distTag: 'latest' })
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' }) await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' })
await execPnpm(['update', '--filter', 'project-2', '--latest']) // update foo only for project-2
await execPnpm(['update', '--filter', 'project-2', '--latest', '@pnpm.e2e/foo'])
// project 1's manifest is unaffected, while project 2 has foo updated // project 1's manifest is unaffected, while project 2 has only foo updated
expect(loadJsonFile<any>('project-1/package.json').dependencies['@pnpm.e2e/foo']).toBe('1.0.0') // eslint-disable-line expect(loadJsonFile<any>('project-1/package.json').dependencies['@pnpm.e2e/foo']).toBe('1.0.0') // eslint-disable-line
expect(loadJsonFile<any>('project-1/package.json').dependencies['@pnpm.e2e/bar']).toBe('100.0.0') // eslint-disable-line expect(loadJsonFile<any>('project-1/package.json').dependencies['@pnpm.e2e/bar']).toBe('100.0.0') // eslint-disable-line
expect(loadJsonFile<any>('project-2/package.json').dependencies['@pnpm.e2e/foo']).toBe('2.0.0') // eslint-disable-line expect(loadJsonFile<any>('project-2/package.json').dependencies['@pnpm.e2e/foo']).toBe('2.0.0') // eslint-disable-line
expect(loadJsonFile<any>('project-2/package.json').dependencies['@pnpm.e2e/bar']).toBe('100.0.0') // eslint-disable-line
// similar for the importers in the lockfile; project 1 is unaffected, while // similar for the importers in the lockfile; project 1 is unaffected, while
// project 2 resolves the latest foo // project 2 resolves the latest foo, but keeps bar to the previous version
const lockfile = readYamlFile<any>(path.resolve(WANTED_LOCKFILE)) // eslint-disable-line const lockfile = readYamlFile<any>(path.resolve(WANTED_LOCKFILE)) // eslint-disable-line
expect(lockfile.importers['project-1']?.dependencies?.['@pnpm.e2e/foo'].version).toStrictEqual('1.0.0') expect(lockfile.importers['project-1']?.dependencies?.['@pnpm.e2e/foo'].version).toStrictEqual('1.0.0')
expect(lockfile.importers['project-1']?.dependencies?.['@pnpm.e2e/bar'].version).toStrictEqual('100.0.0') expect(lockfile.importers['project-1']?.dependencies?.['@pnpm.e2e/bar'].version).toStrictEqual('100.0.0')
expect(lockfile.importers['project-2']?.dependencies?.['@pnpm.e2e/foo'].version).toStrictEqual('2.0.0') expect(lockfile.importers['project-2']?.dependencies?.['@pnpm.e2e/foo'].version).toStrictEqual('2.0.0')
expect(lockfile.importers['project-2']?.dependencies?.['@pnpm.e2e/bar'].version).toStrictEqual('100.0.0')
}) })
// Covers https://github.com/pnpm/pnpm/issues/6154 // Covers https://github.com/pnpm/pnpm/issues/6154

View File

@@ -126,12 +126,11 @@ export interface RequestPackageOptions {
registry: string registry: string
sideEffectsCache?: boolean sideEffectsCache?: boolean
skipFetch?: boolean skipFetch?: boolean
update?: boolean update?: false | 'compatible' | 'latest'
workspacePackages?: WorkspacePackages workspacePackages?: WorkspacePackages
forceResolve?: boolean forceResolve?: boolean
supportedArchitectures?: SupportedArchitectures supportedArchitectures?: SupportedArchitectures
onFetchError?: OnFetchError onFetchError?: OnFetchError
updateToLatest?: boolean
injectWorkspacePackages?: boolean injectWorkspacePackages?: boolean
} }