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,
registry: options.registry,
workspacePackages: options.workspacePackages,
updateToLatest: options.updateToLatest,
updateToLatest: options.update === 'latest',
injectWorkspacePackages: options.injectWorkspacePackages,
}), { priority: options.downloadPriority })

View File

@@ -15,7 +15,7 @@ import loadJsonFile from 'load-json-file'
import nock from 'nock'
import normalize from 'normalize-path'
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 f = fixtures(__dirname)
@@ -182,7 +182,7 @@ test('refetch local tarball if its integrity has changed', async () => {
registry,
skipFetch: true,
update: false,
}
} satisfies RequestPackageOptions
{
const requestPackage = createPackageRequester({
@@ -288,7 +288,7 @@ test('refetch local tarball if its integrity has changed. The requester does not
projectDir,
registry,
update: false,
}
} satisfies RequestPackageOptions
{
const requestPackage = createPackageRequester({

View File

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

View File

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