fix(update): prevent package.json updates when updating indirect dependencies (#5118) (#10155)

close #5118
This commit is contained in:
Trevor Burnham
2025-11-06 09:13:51 -05:00
committed by Zoltan Kochan
parent df80112cfe
commit 17344ca27f
3 changed files with 55 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-installation": patch
"pnpm": patch
---
When a user runs `pnpm update` on a dependency that is not directly listed in `package.json`, none of the direct dependencies should be updated [#10155](https://github.com/pnpm/pnpm/pull/10155).

View File

@@ -272,6 +272,8 @@ when running add/update with the --workspace option')
}
let updateMatch: UpdateDepsMatcher | null
let updatePackageManifest = opts.updatePackageManifest
let updateMatching: ((pkgName: string) => boolean) | undefined
if (opts.update) {
if (params.length === 0) {
const ignoreDeps = opts.updateConfig?.ignoreDependencies
@@ -291,6 +293,10 @@ when running add/update with the --workspace option')
throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES',
'None of the specified packages were found in the dependencies.')
}
// No direct dependencies matched, so we're updating indirect dependencies only
// Don't update package.json in this case, and limit updates to only matching dependencies
updatePackageManifest = false
updateMatching = (pkgName: string) => updateMatch!(pkgName) != null
}
}
@@ -343,7 +349,11 @@ when running add/update with the --workspace option')
return
}
const { updatedCatalogs, updatedManifest, ignoredBuilds } = await install(manifest, installOpts)
const { updatedCatalogs, updatedManifest, ignoredBuilds } = await install(manifest, {
...installOpts,
updatePackageManifest,
updateMatching,
})
if (opts.update === true && opts.save !== false) {
await Promise.all([
writeProjectManifest(updatedManifest),

View File

@@ -684,6 +684,44 @@ test('update with tag @latest will downgrade prerelease', async function () {
expect(lockfile2).toHaveProperty(['packages', '@pnpm.e2e/has-prerelease@2.0.0'])
})
test('update indirect dependency should not update package.json', async function () {
const project = prepare({
dependencies: {
'@pnpm.e2e/pkg-with-1-dep': '^100.0.0',
},
})
// Ensure the initial versions
await addDistTag('@pnpm.e2e/pkg-with-1-dep', '100.0.0', 'latest')
await addDistTag('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0', 'latest')
await execPnpm(['install'])
const pkg1 = await readPackageJsonFromDir(process.cwd())
expect(pkg1.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.0.0')
const lockfile1 = project.readLockfile()
expect(lockfile1.importers['.'].dependencies?.['@pnpm.e2e/pkg-with-1-dep'].version).toBe('100.0.0')
// Now publish a new version of the direct dependency and update the indirect dependency
await addDistTag('@pnpm.e2e/pkg-with-1-dep', '100.1.0', 'latest')
await addDistTag('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.1.0', 'latest')
// Update the indirect dependency only
await execPnpm(['update', '@pnpm.e2e/dep-of-pkg-with-1-dep@latest'])
// The direct dependency in package.json should remain unchanged at ^100.0.0
const pkg2 = await readPackageJsonFromDir(process.cwd())
expect(pkg2.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.0.0')
// But the lockfile should have the updated indirect dependency
const lockfile2 = project.readLockfile()
expect(Object.keys(lockfile2.packages ?? {})).toContain('@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0')
// The direct dependency should remain at 100.0.0 in the lockfile (not upgraded to 100.1.0)
expect(lockfile2.importers['.'].dependencies?.['@pnpm.e2e/pkg-with-1-dep'].version).toBe('100.0.0')
})
test('update to latest recursive workspace (outdated, updated, prerelease, outdated)', async function () {
await addDistTag('@pnpm.e2e/has-prerelease', '2.0.0', 'latest')