mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-09 08:54:57 -04:00
fix: sync packageManager and devEngines.packageManager on self-update (#11395)
* fix: sync packageManager and devEngines.packageManager on self-update When `package.json` declares both `packageManager` and `devEngines.packageManager`, `pnpm self-update` previously bumped only the latter — leaving Corepack (which reads `packageManager`) pinned to the old version until a manual edit. Now, when `packageManager` pins pnpm, both fields are rewritten to the new exact version on update: `packageManager` to `pnpm@<version>` (without an integrity hash) and `devEngines.packageManager.version` to the same exact `<version>` (dropping any range operator). When only `devEngines.packageManager` is declared, the existing range-preserving behavior is unchanged. Closes #11388 * refactor: export and reuse parsePackageManager from @pnpm/config.reader Drop the inline duplicate in self-updater and use the existing parser from config.reader. Same parsing rules (strips integrity hash, rejects URL-style refs). * refactor: collapse devEngines.packageManager array/object branches Resolve to the underlying pnpm entry first (whether the field is an array or an object) and run the version-update logic once, instead of duplicating it across both branches.
This commit is contained in:
7
.changeset/self-update-syncs-package-manager-fields.md
Normal file
7
.changeset/self-update-syncs-package-manager-fields.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/config.reader": patch
|
||||
"@pnpm/engine.pm.commands": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
`pnpm self-update` now keeps `package.json`'s `packageManager` and `devEngines.packageManager` in sync. When the legacy `packageManager` field pins pnpm, both fields are rewritten to the new exact pnpm version on update — `packageManager` to `pnpm@<version>` (without an integrity hash), and `devEngines.packageManager.version` to the same exact `<version>` (dropping any range operator). When only `devEngines.packageManager` is declared, the existing range-preserving behavior is unchanged [#11388](https://github.com/pnpm/pnpm/issues/11388).
|
||||
@@ -693,7 +693,7 @@ function getWantedPackageManager (manifest: ProjectManifest): { pm?: WantedPacka
|
||||
return { warnings }
|
||||
}
|
||||
|
||||
function parsePackageManager (packageManager: string): { name: string, version: string | undefined } {
|
||||
export function parsePackageManager (packageManager: string): { name: string, version: string | undefined } {
|
||||
if (!packageManager.includes('@')) return { name: packageManager, version: undefined }
|
||||
const [name, pmReference] = packageManager.split('@')
|
||||
// pmReference is semantic versioning, not URL
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'node:path'
|
||||
import { linkBins } from '@pnpm/bins.linker'
|
||||
import { isExecutedByCorepack, packageManager } from '@pnpm/cli.meta'
|
||||
import { docsUrl } from '@pnpm/cli.utils'
|
||||
import { type Config, type ConfigContext, types as allTypes } from '@pnpm/config.reader'
|
||||
import { type Config, type ConfigContext, parsePackageManager, types as allTypes } from '@pnpm/config.reader'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { createResolver } from '@pnpm/installing.client'
|
||||
import { resolvePackageManagerIntegrities } from '@pnpm/installing.env-installer'
|
||||
@@ -113,22 +113,38 @@ export async function handler (
|
||||
if (opts.wantedPackageManager?.version !== resolution.manifest.version) {
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(opts.rootProjectManifestDir)
|
||||
if (manifest.devEngines?.packageManager) {
|
||||
if (Array.isArray(manifest.devEngines.packageManager)) {
|
||||
const pnpmEntry = manifest.devEngines.packageManager.find((e) => e.name === 'pnpm')
|
||||
if (pnpmEntry) {
|
||||
const updated = updateVersionConstraint(pnpmEntry.version, resolution.manifest.version)
|
||||
if (updated !== pnpmEntry.version) {
|
||||
pnpmEntry.version = updated
|
||||
await writeProjectManifest(manifest)
|
||||
}
|
||||
}
|
||||
} else if (manifest.devEngines.packageManager.name === 'pnpm') {
|
||||
const updated = updateVersionConstraint(manifest.devEngines.packageManager.version, resolution.manifest.version)
|
||||
if (updated !== manifest.devEngines.packageManager.version) {
|
||||
manifest.devEngines.packageManager.version = updated
|
||||
await writeProjectManifest(manifest)
|
||||
let manifestChanged = false
|
||||
// If "packageManager" pins pnpm, treat both fields as the user's
|
||||
// single source of truth for the active pnpm version: rewrite both
|
||||
// to the new exact version (dropping any range operator in
|
||||
// devEngines and any integrity hash on the legacy field). When only
|
||||
// devEngines is set, preserve the user's range style and let the
|
||||
// lockfile pin the exact version.
|
||||
const legacyPm = manifest.packageManager != null
|
||||
? parsePackageManager(manifest.packageManager)
|
||||
: undefined
|
||||
const legacyPinsPnpm = legacyPm?.name === 'pnpm' && legacyPm.version != null
|
||||
const devEnginesPm = manifest.devEngines.packageManager
|
||||
const pnpmEntry = Array.isArray(devEnginesPm)
|
||||
? devEnginesPm.find((e) => e.name === 'pnpm')
|
||||
: devEnginesPm.name === 'pnpm' ? devEnginesPm : undefined
|
||||
if (pnpmEntry) {
|
||||
const updated = legacyPinsPnpm
|
||||
? resolution.manifest.version
|
||||
: updateVersionConstraint(pnpmEntry.version, resolution.manifest.version)
|
||||
if (updated !== pnpmEntry.version) {
|
||||
pnpmEntry.version = updated
|
||||
manifestChanged = true
|
||||
}
|
||||
}
|
||||
if (legacyPinsPnpm) {
|
||||
const newLegacy = `pnpm@${resolution.manifest.version}`
|
||||
if (manifest.packageManager !== newLegacy) {
|
||||
manifest.packageManager = newLegacy
|
||||
manifestChanged = true
|
||||
}
|
||||
}
|
||||
if (manifestChanged) await writeProjectManifest(manifest)
|
||||
const store = await createStoreController(opts)
|
||||
await resolvePackageManagerIntegrities(resolution.manifest.version, {
|
||||
registries: opts.registries,
|
||||
|
||||
@@ -367,6 +367,137 @@ test('should fall back to ^version when complex range cannot accommodate the new
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('^9.0.0')
|
||||
})
|
||||
|
||||
test('should update both packageManager and devEngines.packageManager when both pin the same exact version', async () => {
|
||||
const opts = prepare({
|
||||
packageManager: 'pnpm@8.0.0',
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '8.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
getMockAgent().get(opts.registries.default.replace(/\/$/, ''))
|
||||
.intercept({ path: '/pnpm', method: 'GET' })
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default)).persist()
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
const output = await selfUpdate.handler({
|
||||
...opts,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '8.0.0',
|
||||
},
|
||||
}, [])
|
||||
|
||||
expect(output).toBe('The current project has been updated to use pnpm v9.0.0')
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
expect(pkgJson.packageManager).toBe('pnpm@9.0.0')
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('9.0.0')
|
||||
})
|
||||
|
||||
test('should update both packageManager (with integrity hash) and devEngines.packageManager when versions agree', async () => {
|
||||
const opts = prepare({
|
||||
packageManager: 'pnpm@8.0.0+sha512.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '8.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
getMockAgent().get(opts.registries.default.replace(/\/$/, ''))
|
||||
.intercept({ path: '/pnpm', method: 'GET' })
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default)).persist()
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
await selfUpdate.handler({
|
||||
...opts,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '8.0.0',
|
||||
},
|
||||
}, [])
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
expect(pkgJson.packageManager).toBe('pnpm@9.0.0')
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('9.0.0')
|
||||
})
|
||||
|
||||
test('should sync both fields to the new exact version when their current versions disagree', async () => {
|
||||
const opts = prepare({
|
||||
packageManager: 'pnpm@7.0.0',
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '8.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
getMockAgent().get(opts.registries.default.replace(/\/$/, ''))
|
||||
.intercept({ path: '/pnpm', method: 'GET' })
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default)).persist()
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
await selfUpdate.handler({
|
||||
...opts,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '8.0.0',
|
||||
},
|
||||
}, [])
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
expect(pkgJson.packageManager).toBe('pnpm@9.0.0')
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('9.0.0')
|
||||
})
|
||||
|
||||
test('should pin devEngines.packageManager to an exact version when packageManager also pins pnpm', async () => {
|
||||
const opts = prepare({
|
||||
packageManager: 'pnpm@8.0.0',
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '^8.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
getMockAgent().get(opts.registries.default.replace(/\/$/, ''))
|
||||
.intercept({ path: '/pnpm', method: 'GET' })
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default)).persist()
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
await selfUpdate.handler({
|
||||
...opts,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '^8.0.0',
|
||||
},
|
||||
}, [])
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
expect(pkgJson.packageManager).toBe('pnpm@9.0.0')
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('9.0.0')
|
||||
})
|
||||
|
||||
test('should leave packageManager alone when it pins a different package manager', async () => {
|
||||
const opts = prepare({
|
||||
packageManager: 'yarn@4.0.0',
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '^8.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
getMockAgent().get(opts.registries.default.replace(/\/$/, ''))
|
||||
.intercept({ path: '/pnpm', method: 'GET' })
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default)).persist()
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
await selfUpdate.handler({
|
||||
...opts,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '^8.0.0',
|
||||
},
|
||||
}, [])
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
expect(pkgJson.packageManager).toBe('yarn@4.0.0')
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('^9.0.0')
|
||||
})
|
||||
|
||||
test('should update devEngines.packageManager range when resolved version no longer satisfies it', async () => {
|
||||
const opts = prepare({
|
||||
devEngines: {
|
||||
|
||||
Reference in New Issue
Block a user