mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
feat: support devEngines.packageManager for pnpm version management (#10932)
## Summary - Support specifying the pnpm version via `devEngines.packageManager` in `package.json`, as an alternative to the `packageManager` field - Unlike `packageManager`, `devEngines.packageManager` supports semver ranges — the resolved version is stored in `pnpm-lock.env.yaml` and reused if it still satisfies the range - The `onFail` field determines behavior: `download` (auto-download), `error` (default), `warn`, or `ignore` - `devEngines.packageManager` takes precedence over `packageManager` when both are present (with a warning) - For array notation, default `onFail` is `ignore` for non-last elements and `error` for the last - For the legacy `packageManager` field, `onFail` is derived from existing config settings (`managePackageManagerVersions`, `packageManagerStrict`, `packageManagerStrictVersion`), so `main.ts` uses `onFail` as the single source of truth - Reuses `EngineDependency` type from `@pnpm/types` instead of a custom `WantedPackageManager` type ## Test plan - [x] 10 tests in `switchingVersions.test.ts` — version switching with `packageManager` field, `devEngines.packageManager` with `onFail=download` (exact + range), env lockfile reuse, corrupt binary - [x] 15 tests in `packageManagerCheck.test.ts` — version checks with `engines.pnpm`, `packageManager` field, `devEngines.packageManager` with all `onFail` values, array notation, range matching, precedence close https://github.com/pnpm/pnpm/issues/8153
This commit is contained in:
6
.changeset/dev-engines-package-manager.md
Normal file
6
.changeset/dev-engines-package-manager.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Support specifying the pnpm version via `devEngines.packageManager` in `package.json`. Unlike the `packageManager` field, this supports version ranges. The resolved version is stored in `pnpm-lock.env.yaml` and reused if it still satisfies the range.
|
||||
@@ -60,6 +60,7 @@
|
||||
"ramda": "catalog:",
|
||||
"read-ini-file": "catalog:",
|
||||
"realpath-missing": "catalog:",
|
||||
"semver": "catalog:",
|
||||
"which": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -73,6 +74,7 @@
|
||||
"@types/is-windows": "catalog:",
|
||||
"@types/lodash.kebabcase": "catalog:",
|
||||
"@types/ramda": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@types/which": "catalog:",
|
||||
"symlink-dir": "catalog:",
|
||||
"write-yaml-file": "catalog:"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Catalogs } from '@pnpm/catalogs.types'
|
||||
import type {
|
||||
EngineDependency,
|
||||
Finder,
|
||||
Project,
|
||||
ProjectManifest,
|
||||
@@ -14,10 +15,6 @@ import type { AuthInfo } from './parseAuthInfo.js'
|
||||
|
||||
export type UniversalOptions = Pick<Config, 'color' | 'dir' | 'rawConfig' | 'rawLocalConfig'>
|
||||
|
||||
export interface WantedPackageManager {
|
||||
name: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
export type VerifyDepsBeforeRun = 'install' | 'warn' | 'error' | 'prompt' | false
|
||||
|
||||
@@ -89,7 +86,7 @@ export interface Config extends AuthInfo, OptionsFromRootManifest {
|
||||
name: string
|
||||
version: string
|
||||
}
|
||||
wantedPackageManager?: WantedPackageManager
|
||||
wantedPackageManager?: EngineDependency
|
||||
preferOffline?: boolean
|
||||
sideEffectsCache?: boolean // for backward compatibility
|
||||
sideEffectsCacheReadonly?: boolean // for backward compatibility
|
||||
|
||||
@@ -12,12 +12,13 @@ import type npmTypes from '@pnpm/npm-conf/lib/types.js'
|
||||
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
||||
import { getCurrentBranch } from '@pnpm/git-utils'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import type { ProjectManifest } from '@pnpm/types'
|
||||
import type { DevEngines, EngineDependency, ProjectManifest } from '@pnpm/types'
|
||||
import { betterPathResolve } from 'better-path-resolve'
|
||||
import camelcase from 'camelcase'
|
||||
import isWindows from 'is-windows'
|
||||
import kebabCase from 'lodash.kebabcase'
|
||||
import normalizeRegistryUrl from 'normalize-registry-url'
|
||||
import semver from 'semver'
|
||||
import { realpathMissing } from 'realpath-missing'
|
||||
import { pathAbsolute } from 'path-absolute'
|
||||
import which from 'which'
|
||||
@@ -34,7 +35,6 @@ import type {
|
||||
ProjectConfig,
|
||||
UniversalOptions,
|
||||
VerifyDepsBeforeRun,
|
||||
WantedPackageManager,
|
||||
} from './Config.js'
|
||||
import { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concurrency.js'
|
||||
import { parseEnvVars } from './env.js'
|
||||
@@ -64,7 +64,7 @@ export {
|
||||
createProjectConfigRecord,
|
||||
} from './projectConfig.js'
|
||||
|
||||
export type { Config, ProjectConfig, UniversalOptions, WantedPackageManager, VerifyDepsBeforeRun }
|
||||
export type { Config, ProjectConfig, UniversalOptions, VerifyDepsBeforeRun }
|
||||
|
||||
export { isIniConfigKey } from './auth.js'
|
||||
export { type ConfigFileKey, isConfigFileKey } from './configFileKey.js'
|
||||
@@ -394,9 +394,11 @@ export async function getConfig (opts: {
|
||||
if (pnpmConfig.rootProjectManifest.workspaces?.length && !pnpmConfig.workspaceDir) {
|
||||
warnings.push('The "workspaces" field in package.json is not supported by pnpm. Create a "pnpm-workspace.yaml" file instead.')
|
||||
}
|
||||
if (pnpmConfig.rootProjectManifest.packageManager) {
|
||||
pnpmConfig.wantedPackageManager = parsePackageManager(pnpmConfig.rootProjectManifest.packageManager)
|
||||
const wantedPmResult = getWantedPackageManager(pnpmConfig.rootProjectManifest)
|
||||
if (wantedPmResult.pm) {
|
||||
pnpmConfig.wantedPackageManager = wantedPmResult.pm
|
||||
}
|
||||
warnings.push(...wantedPmResult.warnings)
|
||||
if (pnpmConfig.rootProjectManifest) {
|
||||
Object.assign(pnpmConfig, getOptionsFromRootManifest(pnpmConfig.rootProjectManifestDir, pnpmConfig.rootProjectManifest))
|
||||
}
|
||||
@@ -623,6 +625,20 @@ export async function getConfig (opts: {
|
||||
|
||||
transformPathKeys(pnpmConfig, os.homedir())
|
||||
|
||||
// For the legacy packageManager field, derive onFail from config settings.
|
||||
// devEngines.packageManager already has onFail set during parsing.
|
||||
if (pnpmConfig.wantedPackageManager && pnpmConfig.wantedPackageManager.onFail == null) {
|
||||
if (pnpmConfig.packageManagerStrict === false) {
|
||||
pnpmConfig.wantedPackageManager.onFail = 'warn'
|
||||
} else if (pnpmConfig.managePackageManagerVersions) {
|
||||
pnpmConfig.wantedPackageManager.onFail = 'download'
|
||||
} else if (pnpmConfig.packageManagerStrictVersion) {
|
||||
pnpmConfig.wantedPackageManager.onFail = 'error'
|
||||
} else {
|
||||
pnpmConfig.wantedPackageManager.onFail = 'ignore'
|
||||
}
|
||||
}
|
||||
|
||||
return { config: pnpmConfig, warnings }
|
||||
}
|
||||
|
||||
@@ -632,6 +648,36 @@ function getProcessEnv (env: string): string | undefined {
|
||||
process.env[env.toLowerCase()]
|
||||
}
|
||||
|
||||
function getWantedPackageManager (manifest: ProjectManifest): { pm?: EngineDependency, warnings: string[] } {
|
||||
const warnings: string[] = []
|
||||
const pmFromDevEngines = parseDevEnginesPackageManager(manifest.devEngines)
|
||||
if (pmFromDevEngines) {
|
||||
if (pmFromDevEngines.version != null && !semver.validRange(pmFromDevEngines.version)) {
|
||||
warnings.push(`Cannot use devEngines.packageManager version "${pmFromDevEngines.version}": not a valid version or range`)
|
||||
pmFromDevEngines.version = undefined
|
||||
}
|
||||
if (manifest.packageManager) {
|
||||
warnings.push('Cannot use both "packageManager" and "devEngines.packageManager" in package.json. "packageManager" will be ignored')
|
||||
}
|
||||
return { pm: pmFromDevEngines, warnings }
|
||||
}
|
||||
if (manifest.packageManager) {
|
||||
const pm = parsePackageManager(manifest.packageManager)
|
||||
if (pm.version != null) {
|
||||
const cleanVersion = semver.valid(pm.version)
|
||||
if (!cleanVersion) {
|
||||
warnings.push(`Cannot use packageManager "${manifest.packageManager}": "${pm.version}" is not a valid exact version`)
|
||||
pm.version = undefined
|
||||
} else if (cleanVersion !== pm.version) {
|
||||
warnings.push(`Cannot use packageManager "${manifest.packageManager}": you need to specify the version as "${cleanVersion}"`)
|
||||
pm.version = undefined
|
||||
}
|
||||
}
|
||||
return { pm, warnings }
|
||||
}
|
||||
return { warnings }
|
||||
}
|
||||
|
||||
function parsePackageManager (packageManager: string): { name: string, version: string | undefined } {
|
||||
if (!packageManager.includes('@')) return { name: packageManager, version: undefined }
|
||||
const [name, pmReference] = packageManager.split('@')
|
||||
@@ -645,6 +691,36 @@ function parsePackageManager (packageManager: string): { name: string, version:
|
||||
}
|
||||
}
|
||||
|
||||
function parseDevEnginesPackageManager (devEngines?: DevEngines): EngineDependency | undefined {
|
||||
if (!devEngines?.packageManager) return undefined
|
||||
let pmEngine: EngineDependency | undefined
|
||||
let onFail: 'ignore' | 'warn' | 'error' | 'download'
|
||||
if (Array.isArray(devEngines.packageManager)) {
|
||||
const engines = devEngines.packageManager
|
||||
if (engines.length === 0) return undefined
|
||||
const pnpmIndex = engines.findIndex((engine) => engine.name === 'pnpm')
|
||||
if (pnpmIndex !== -1) {
|
||||
pmEngine = engines[pnpmIndex]
|
||||
// In array notation, default onFail is 'error' for the last element, 'ignore' for others.
|
||||
onFail = pmEngine.onFail ?? (pnpmIndex === engines.length - 1 ? 'error' : 'ignore')
|
||||
} else {
|
||||
pmEngine = engines[0]
|
||||
// No pnpm entry found — use the last element's onFail for the overall failure behavior.
|
||||
const lastEngine = engines[engines.length - 1]
|
||||
onFail = lastEngine.onFail ?? 'error'
|
||||
}
|
||||
} else {
|
||||
pmEngine = devEngines.packageManager
|
||||
onFail = pmEngine.onFail ?? 'error'
|
||||
}
|
||||
if (!pmEngine?.name) return undefined
|
||||
return {
|
||||
name: pmEngine.name,
|
||||
version: pmEngine.version,
|
||||
onFail,
|
||||
}
|
||||
}
|
||||
|
||||
function addSettingsFromWorkspaceManifestToConfig (pnpmConfig: Config, {
|
||||
configFromCliOpts,
|
||||
projectManifest,
|
||||
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -2023,6 +2023,9 @@ importers:
|
||||
realpath-missing:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0
|
||||
semver:
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.4
|
||||
which:
|
||||
specifier: 'catalog:'
|
||||
version: '@pnpm/which@3.0.1'
|
||||
@@ -2048,6 +2051,9 @@ importers:
|
||||
'@types/ramda':
|
||||
specifier: 'catalog:'
|
||||
version: 0.29.12
|
||||
'@types/semver':
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.1
|
||||
'@types/which':
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.2
|
||||
@@ -8980,6 +8986,9 @@ importers:
|
||||
'@pnpm/logger':
|
||||
specifier: 'catalog:'
|
||||
version: 1001.0.1
|
||||
'@pnpm/npm-resolver':
|
||||
specifier: workspace:*
|
||||
version: link:../../resolving/npm-resolver
|
||||
'@pnpm/package-store':
|
||||
specifier: workspace:*
|
||||
version: link:../../store/package-store
|
||||
@@ -8998,6 +9007,9 @@ importers:
|
||||
render-help:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0
|
||||
semver:
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.4
|
||||
symlink-dir:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
@@ -9020,6 +9032,9 @@ importers:
|
||||
'@types/ramda':
|
||||
specifier: 'catalog:'
|
||||
version: 0.29.12
|
||||
'@types/semver':
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.1
|
||||
cross-spawn:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.6
|
||||
|
||||
@@ -9,14 +9,16 @@ if (!global['pnpm__startedAt']) {
|
||||
import loudRejection from 'loud-rejection'
|
||||
import { packageManager, isExecutedByCorepack } from '@pnpm/cli-meta'
|
||||
import { getConfig, installConfigDepsAndLoadHooks } from '@pnpm/cli-utils'
|
||||
import type { Config, WantedPackageManager } from '@pnpm/config'
|
||||
import type { Config } from '@pnpm/config'
|
||||
import { executionTimeLogger, scopeLogger } from '@pnpm/core-loggers'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { filterPackagesFromDir } from '@pnpm/filter-workspace-packages'
|
||||
import { globalWarn, logger } from '@pnpm/logger'
|
||||
import type { ParsedCliArgs } from '@pnpm/parse-cli-args'
|
||||
import type { EngineDependency } from '@pnpm/types'
|
||||
import { finishWorkers } from '@pnpm/worker'
|
||||
import chalk from 'chalk'
|
||||
import semver from 'semver'
|
||||
import path from 'path'
|
||||
import { isEmpty } from 'ramda'
|
||||
import { stripVTControlCharacters as stripAnsi } from 'util'
|
||||
@@ -112,13 +114,14 @@ export async function main (inputArgv: string[]): Promise<void> {
|
||||
ignoreNonAuthSettingsFromLocal: isDlxOrCreateCommand,
|
||||
}) as typeof config
|
||||
if (!isExecutedByCorepack() && cmd !== 'setup' && config.wantedPackageManager != null) {
|
||||
if (config.managePackageManagerVersions && config.wantedPackageManager?.name === 'pnpm' && cmd !== 'self-update') {
|
||||
const pm = config.wantedPackageManager
|
||||
if (pm.onFail === 'download' && pm.name === 'pnpm' && cmd !== 'self-update') {
|
||||
await switchCliVersion(config)
|
||||
} else if (!cmd || !skipPackageManagerCheckForCommand.has(cmd)) {
|
||||
} else if (pm.onFail !== 'ignore' && (!cmd || !skipPackageManagerCheckForCommand.has(cmd))) {
|
||||
if (cliOptions.global) {
|
||||
globalWarn('Using --global skips the package manager check for this project')
|
||||
} else {
|
||||
checkPackageManager(config.wantedPackageManager, config)
|
||||
checkPackageManager(pm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,23 +337,24 @@ function printError (message: string, hint?: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function checkPackageManager (pm: WantedPackageManager, config: Config): void {
|
||||
function checkPackageManager (pm: EngineDependency): void {
|
||||
if (!pm.name) return
|
||||
const shouldError = pm.onFail === 'error' || pm.onFail === 'download'
|
||||
if (pm.name !== 'pnpm') {
|
||||
const msg = `This project is configured to use ${pm.name}`
|
||||
if (config.packageManagerStrict) {
|
||||
if (shouldError) {
|
||||
throw new PnpmError('OTHER_PM_EXPECTED', msg)
|
||||
}
|
||||
globalWarn(msg)
|
||||
} else {
|
||||
} else if (pm.version) {
|
||||
const currentPnpmVersion = packageManager.name === 'pnpm'
|
||||
? packageManager.version
|
||||
: undefined
|
||||
if (currentPnpmVersion && config.packageManagerStrictVersion && pm.version && pm.version !== currentPnpmVersion) {
|
||||
const msg = `This project is configured to use v${pm.version} of pnpm. Your current pnpm is v${currentPnpmVersion}`
|
||||
if (config.packageManagerStrict) {
|
||||
if (currentPnpmVersion && !semver.satisfies(currentPnpmVersion, pm.version, { includePrerelease: true })) {
|
||||
const msg = `This project is configured to use ${pm.version} of pnpm. Your current pnpm is v${currentPnpmVersion}`
|
||||
if (shouldError) {
|
||||
throw new PnpmError('BAD_PM_VERSION', msg, {
|
||||
hint: 'If you want to bypass this version check, you can set the "package-manager-strict" configuration to "false" or set the "COREPACK_ENABLE_STRICT" environment variable to "0"',
|
||||
hint: 'If you want to bypass this version check, you can set the "package-manager-strict" configuration to "false" or set the "COREPACK_ENABLE_STRICT" environment variable to "0". If using "devEngines.packageManager", you can set its "onFail" to "warn" or "ignore"',
|
||||
})
|
||||
} else {
|
||||
globalWarn(msg)
|
||||
|
||||
@@ -14,20 +14,29 @@ import semver from 'semver'
|
||||
export async function switchCliVersion (config: Config): Promise<void> {
|
||||
const pm = config.wantedPackageManager
|
||||
if (pm == null || pm.name !== 'pnpm' || pm.version == null) return
|
||||
const pmVersion = semver.valid(pm.version)
|
||||
if (!pmVersion) {
|
||||
globalWarn(`Cannot switch to pnpm@${pm.version}: "${pm.version}" is not a valid version`)
|
||||
return
|
||||
}
|
||||
if (pmVersion !== pm.version.trim()) {
|
||||
globalWarn(`Cannot switch to pnpm@${pm.version}: you need to specify the version as "${pmVersion}"`)
|
||||
return
|
||||
}
|
||||
|
||||
let envLockfile = await readEnvLockfile(config.rootProjectManifestDir) ?? undefined
|
||||
let storeToUse: Awaited<ReturnType<typeof createStoreController>> | undefined
|
||||
|
||||
if (!isPackageManagerResolved(envLockfile, pmVersion)) {
|
||||
// Check if the env lockfile already has a resolved version that satisfies the wanted version/range.
|
||||
let pmVersion = envLockfile?.importers['.'].packageManagerDependencies?.['pnpm']?.version
|
||||
if (!pmVersion || !semver.satisfies(pmVersion, pm.version, { includePrerelease: true })) {
|
||||
// Resolve to an exact version from the registry.
|
||||
storeToUse = await createStoreController(config)
|
||||
envLockfile = await resolvePackageManagerIntegrities(pm.version, {
|
||||
envLockfile,
|
||||
registries: config.registries,
|
||||
rootDir: config.rootProjectManifestDir,
|
||||
storeController: storeToUse.ctrl,
|
||||
storeDir: storeToUse.dir,
|
||||
})
|
||||
pmVersion = envLockfile.importers['.'].packageManagerDependencies?.['pnpm']?.version
|
||||
if (!pmVersion) {
|
||||
globalWarn(`Cannot resolve pnpm version for "${pm.version}"`)
|
||||
await storeToUse?.ctrl.close()
|
||||
return
|
||||
}
|
||||
} else if (!isPackageManagerResolved(envLockfile, pmVersion)) {
|
||||
storeToUse = await createStoreController(config)
|
||||
envLockfile = await resolvePackageManagerIntegrities(pmVersion, {
|
||||
envLockfile,
|
||||
|
||||
@@ -30,7 +30,7 @@ test('install should not fail if the used pnpm version does not satisfy the pnpm
|
||||
const { status, stderr } = execPnpmSync(['install', '--config.manage-package-manager-versions=false', '--config.package-manager-strict-version=true'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project is configured to use v0.0.0 of pnpm. Your current pnpm is')
|
||||
expect(stderr.toString()).toContain('This project is configured to use 0.0.0 of pnpm. Your current pnpm is')
|
||||
})
|
||||
|
||||
test('install should fail if the project requires a different package manager', async () => {
|
||||
@@ -87,3 +87,160 @@ test('some commands should not fail if the required package manager is not pnpm'
|
||||
const { status } = execPnpmSync(['store', 'path'])
|
||||
expect(status).toBe(0)
|
||||
})
|
||||
|
||||
test('devEngines.packageManager with onFail=error should fail on version mismatch', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '0.0.1',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project is configured to use 0.0.1 of pnpm')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager with onFail=warn should warn on version mismatch', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '0.0.1',
|
||||
onFail: 'warn',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(stdout.toString()).toContain('This project is configured to use 0.0.1 of pnpm')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager with onFail=ignore should not check version', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '0.0.1',
|
||||
onFail: 'ignore',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout, stderr } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(stdout.toString()).not.toContain('0.0.1')
|
||||
expect(stderr.toString()).not.toContain('0.0.1')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager defaults to onFail=error', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '0.0.1',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project is configured to use 0.0.1 of pnpm')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager with a different PM name should fail with onFail=error', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'yarn',
|
||||
version: '>=4.0.0',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project is configured to use yarn')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager array selects the pnpm entry', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: [
|
||||
{ name: 'yarn', version: '>=4.0.0', onFail: 'ignore' },
|
||||
{ name: 'pnpm', version: '0.0.1', onFail: 'error' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project is configured to use 0.0.1 of pnpm')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager array defaults onFail to ignore for non-last elements', async () => {
|
||||
const versionProcess = execPnpmSync(['--version'])
|
||||
const pnpmVersion = versionProcess.stdout.toString().trim()
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: [
|
||||
{ name: 'pnpm', version: pnpmVersion },
|
||||
{ name: 'yarn', version: '>=4.0.0' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// pnpm is the first (non-last) element, so onFail defaults to 'ignore'
|
||||
const { status } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(0)
|
||||
})
|
||||
|
||||
test('devEngines.packageManager with version range should match current version', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '>=1.0.0',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status } = execPnpmSync(['install'])
|
||||
|
||||
expect(status).toBe(0)
|
||||
})
|
||||
|
||||
test('devEngines.packageManager takes precedence over packageManager field', async () => {
|
||||
const versionProcess = execPnpmSync(['--version'])
|
||||
const pnpmVersion = versionProcess.stdout.toString().trim()
|
||||
prepare({
|
||||
packageManager: `pnpm@${pnpmVersion}`,
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '0.0.1',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['install'])
|
||||
|
||||
// devEngines.packageManager takes effect, so version mismatch error is thrown
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project is configured to use 0.0.1 of pnpm')
|
||||
expect(stderr.toString()).toContain('"packageManager" will be ignored')
|
||||
})
|
||||
|
||||
@@ -43,9 +43,9 @@ test('do not switch to pnpm version that is specified not with a semver version'
|
||||
packageManager: 'pnpm@kevva/is-positive',
|
||||
})
|
||||
|
||||
const { stdout } = execPnpmSync(['help'], { env })
|
||||
const { stderr } = execPnpmSync(['help'], { env })
|
||||
|
||||
expect(stdout.toString()).toContain('Cannot switch to pnpm@kevva/is-positive')
|
||||
expect(stderr.toString()).toContain('"kevva/is-positive" is not a valid exact version')
|
||||
})
|
||||
|
||||
test('do not switch to pnpm version that is specified starting with v', async () => {
|
||||
@@ -56,12 +56,12 @@ test('do not switch to pnpm version that is specified starting with v', async ()
|
||||
packageManager: 'pnpm@v9.15.5',
|
||||
})
|
||||
|
||||
const { stdout } = execPnpmSync(['help'], { env })
|
||||
const { stderr } = execPnpmSync(['help'], { env })
|
||||
|
||||
expect(stdout.toString()).toContain('Cannot switch to pnpm@v9.15.5: you need to specify the version as "9.15.5"')
|
||||
expect(stderr.toString()).toContain('you need to specify the version as "9.15.5"')
|
||||
})
|
||||
|
||||
test('do not switch to pnpm version when a range is specified', async () => {
|
||||
test('do not switch to pnpm version when a range is specified in packageManager field', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
@@ -69,9 +69,132 @@ test('do not switch to pnpm version when a range is specified', async () => {
|
||||
packageManager: 'pnpm@^9.3.0',
|
||||
})
|
||||
|
||||
const { stderr } = execPnpmSync(['help'], { env })
|
||||
|
||||
expect(stderr.toString()).toContain('not a valid exact version')
|
||||
})
|
||||
|
||||
test('switch to the pnpm version resolved from devEngines.packageManager with onFail=download', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
writeJsonFileSync('package.json', {
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '9.3.0',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { stdout } = execPnpmSync(['help'], { env })
|
||||
|
||||
expect(stdout.toString()).toContain('Cannot switch to pnpm@^9.3.0')
|
||||
expect(stdout.toString()).toContain('Version 9.3.0')
|
||||
})
|
||||
|
||||
test('switch to the pnpm version resolved from devEngines.packageManager with a range', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
writeJsonFileSync('package.json', {
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '>=9.1.0 <9.1.4',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { stdout } = execPnpmSync(['help'], { env })
|
||||
|
||||
// Should resolve to the highest version in the range (9.1.3, not 9.1.0)
|
||||
expect(stdout.toString()).toContain('Version 9.1.3')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager with onFail=download reuses resolved version from env lockfile', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
writeJsonFileSync('package.json', {
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '>=9.1.0 <9.1.4',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// First run: resolves and writes env lockfile
|
||||
const firstRun = execPnpmSync(['help'], { env })
|
||||
expect(firstRun.stdout.toString()).toContain('Version 9.1.3')
|
||||
|
||||
// Second run: should reuse the resolved version from env lockfile
|
||||
const secondRun = execPnpmSync(['help'], { env })
|
||||
expect(secondRun.stdout.toString()).toContain('Version 9.1.3')
|
||||
|
||||
// Verify env lockfile was written
|
||||
expect(fs.existsSync('pnpm-lock.env.yaml')).toBe(true)
|
||||
})
|
||||
|
||||
test('devEngines.packageManager re-resolves when locked version no longer satisfies updated range', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
|
||||
// First run: seed the lockfile with 9.1.1
|
||||
writeJsonFileSync('package.json', {
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '>=9.1.0 <9.1.2',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
const firstRun = execPnpmSync(['help'], { env })
|
||||
expect(firstRun.stdout.toString()).toContain('Version 9.1.1')
|
||||
expect(fs.existsSync('pnpm-lock.env.yaml')).toBe(true)
|
||||
|
||||
// Update range so the previously locked 9.1.1 no longer satisfies it
|
||||
writeJsonFileSync('package.json', {
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '>=9.1.2 <9.1.4',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Should re-resolve and switch to 9.1.3
|
||||
const secondRun = execPnpmSync(['help'], { env })
|
||||
expect(secondRun.stdout.toString()).toContain('Version 9.1.3')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager without onFail=download does not switch version', async () => {
|
||||
prepare()
|
||||
const pnpmHome = path.resolve('pnpm')
|
||||
const env = { PNPM_HOME: pnpmHome }
|
||||
writeYamlFileSync('pnpm-workspace.yaml', {
|
||||
managePackageManagerVersions: false,
|
||||
})
|
||||
writeJsonFileSync('package.json', {
|
||||
devEngines: {
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '9.3.0',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout } = execPnpmSync(['help'], { env })
|
||||
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).not.toContain('Version 9.3.0')
|
||||
})
|
||||
|
||||
test('throws error if pnpm binary in store is corrupt', () => {
|
||||
|
||||
@@ -115,6 +115,7 @@ export {
|
||||
type RegistryPackageSpec,
|
||||
RegistryResponseError,
|
||||
}
|
||||
export { whichVersionIsPinned } from './whichVersionIsPinned.js'
|
||||
|
||||
export interface ResolverFactoryOptions {
|
||||
cacheDir: string
|
||||
|
||||
@@ -43,12 +43,14 @@
|
||||
"@pnpm/headless": "workspace:*",
|
||||
"@pnpm/link-bins": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/npm-resolver": "workspace:*",
|
||||
"@pnpm/package-store": "workspace:*",
|
||||
"@pnpm/read-project-manifest": "workspace:*",
|
||||
"@pnpm/store-connection-manager": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"ramda": "catalog:",
|
||||
"render-help": "catalog:",
|
||||
"semver": "catalog:",
|
||||
"symlink-dir": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -61,6 +63,7 @@
|
||||
"@pnpm/tools.plugin-commands-self-updater": "workspace:*",
|
||||
"@types/cross-spawn": "catalog:",
|
||||
"@types/ramda": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"cross-spawn": "catalog:",
|
||||
"nock": "catalog:"
|
||||
},
|
||||
|
||||
@@ -9,7 +9,10 @@ import { linkBins } from '@pnpm/link-bins'
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import { readProjectManifest } from '@pnpm/read-project-manifest'
|
||||
import { createStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
|
||||
import { whichVersionIsPinned } from '@pnpm/npm-resolver'
|
||||
import type { PinnedVersion } from '@pnpm/types'
|
||||
import { pick } from 'ramda'
|
||||
import semver from 'semver'
|
||||
import { renderHelp } from 'render-help'
|
||||
import { installPnpm } from './installPnpm.js'
|
||||
|
||||
@@ -25,6 +28,8 @@ export function cliOptionsTypes (): Record<string, unknown> {
|
||||
|
||||
export const commandNames = ['self-update']
|
||||
|
||||
export const skipPackageManagerCheck = true
|
||||
|
||||
export function help (): string {
|
||||
return renderHelp({
|
||||
description: 'Updates pnpm to the latest version (or the one specified)',
|
||||
@@ -68,18 +73,37 @@ export async function handler (
|
||||
throw new PnpmError('CANNOT_RESOLVE_PNPM', `Cannot find "${bareSpecifier}" version of pnpm`)
|
||||
}
|
||||
|
||||
if (opts.wantedPackageManager?.name === packageManager.name && opts.managePackageManagerVersions) {
|
||||
if (opts.wantedPackageManager?.name === packageManager.name) {
|
||||
if (opts.wantedPackageManager?.version !== resolution.manifest.version) {
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(opts.rootProjectManifestDir)
|
||||
manifest.packageManager = `pnpm@${resolution.manifest.version}`
|
||||
await writeProjectManifest(manifest)
|
||||
const store = await createStoreController(opts)
|
||||
await resolvePackageManagerIntegrities(resolution.manifest.version, {
|
||||
registries: opts.registries,
|
||||
rootDir: opts.rootProjectManifestDir,
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
const store = await createStoreController(opts)
|
||||
await resolvePackageManagerIntegrities(resolution.manifest.version, {
|
||||
registries: opts.registries,
|
||||
rootDir: opts.rootProjectManifestDir,
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
})
|
||||
} else {
|
||||
manifest.packageManager = `pnpm@${resolution.manifest.version}`
|
||||
await writeProjectManifest(manifest)
|
||||
}
|
||||
return `The current project has been updated to use pnpm v${resolution.manifest.version}`
|
||||
} else {
|
||||
return `The current project is already set to use pnpm v${resolution.manifest.version}`
|
||||
@@ -114,3 +138,34 @@ export async function handler (
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the updated version constraint for devEngines.packageManager.
|
||||
* - Exact versions and simple ranges (^, ~) are updated to the new version,
|
||||
* preserving the range operator.
|
||||
* - Ranges that still satisfy the new version are returned unchanged
|
||||
* (the exact version will be pinned in the lockfile instead).
|
||||
* - Complex ranges (>=x <y, etc.) that no longer satisfy the new version
|
||||
* fall back to a caret range with the new version (`^${newVersion}`).
|
||||
*/
|
||||
function updateVersionConstraint (current: string | undefined, newVersion: string): string | undefined {
|
||||
if (current == null) return newVersion
|
||||
// Range that still satisfies the new version — leave it as-is (lockfile handles pinning)
|
||||
if (semver.satisfies(newVersion, current, { includePrerelease: true })) return current
|
||||
// Determine the pinning style of the current specifier
|
||||
const pinnedVersion = whichVersionIsPinned(current)
|
||||
if (pinnedVersion == null) {
|
||||
// Complex range that can't be updated while preserving its structure — fall back to ^version
|
||||
return `^${newVersion}`
|
||||
}
|
||||
return versionSpecFromPinned(newVersion, pinnedVersion)
|
||||
}
|
||||
|
||||
function versionSpecFromPinned (version: string, pinnedVersion: PinnedVersion): string {
|
||||
switch (pinnedVersion) {
|
||||
case 'none':
|
||||
case 'major': return `^${version}`
|
||||
case 'minor': return `~${version}`
|
||||
case 'patch': return version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ beforeEach(() => {
|
||||
nock.enableNetConnect()
|
||||
})
|
||||
|
||||
function prepare () {
|
||||
function prepare (manifest: object = {}) {
|
||||
const dir = tempDir(false)
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), '{}', 'utf8')
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(manifest), 'utf8')
|
||||
return prepareOptions(dir)
|
||||
}
|
||||
|
||||
@@ -208,10 +208,8 @@ test('should update packageManager field when a newer pnpm version is available'
|
||||
packageManager: 'pnpm@8.0.0',
|
||||
}), 'utf8')
|
||||
nock(opts.registries.default)
|
||||
.persist()
|
||||
.get('/pnpm')
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default))
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
const output = await selfUpdate.handler({
|
||||
...opts,
|
||||
@@ -249,6 +247,151 @@ test('should not update packageManager field when current version matches latest
|
||||
expect(JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')).packageManager).toBe('pnpm@9.0.0')
|
||||
})
|
||||
|
||||
test('should update devEngines.packageManager version when a newer pnpm version is available', async () => {
|
||||
const opts = prepare({
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '8.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
nock(opts.registries.default)
|
||||
.persist()
|
||||
.get('/pnpm')
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default))
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
const output = await selfUpdate.handler({
|
||||
...opts,
|
||||
managePackageManagerVersions: true,
|
||||
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.devEngines.packageManager.version).toBe('9.0.0')
|
||||
expect(pkgJson.packageManager).toBeUndefined()
|
||||
})
|
||||
|
||||
test('should update pnpm entry in devEngines.packageManager array', async () => {
|
||||
const opts = prepare({
|
||||
devEngines: {
|
||||
packageManager: [
|
||||
{ name: 'npm', version: '10.0.0' },
|
||||
{ name: 'pnpm', version: '8.0.0' },
|
||||
],
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
nock(opts.registries.default)
|
||||
.persist()
|
||||
.get('/pnpm')
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default))
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
const output = await selfUpdate.handler({
|
||||
...opts,
|
||||
managePackageManagerVersions: true,
|
||||
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.devEngines.packageManager[1].version).toBe('9.0.0')
|
||||
expect(pkgJson.devEngines.packageManager[0].version).toBe('10.0.0')
|
||||
expect(pkgJson.packageManager).toBeUndefined()
|
||||
})
|
||||
|
||||
test('should not modify devEngines.packageManager range when resolved version still satisfies it', async () => {
|
||||
const opts = prepare({
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '>=8.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
nock(opts.registries.default)
|
||||
.persist()
|
||||
.get('/pnpm')
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default))
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
const output = await selfUpdate.handler({
|
||||
...opts,
|
||||
managePackageManagerVersions: true,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '>=8.0.0',
|
||||
},
|
||||
}, [])
|
||||
|
||||
expect(output).toBe('The current project has been updated to use pnpm v9.0.0')
|
||||
// The range should remain unchanged — the exact version is pinned in the lockfile
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('>=8.0.0')
|
||||
// The lockfile should be written with the resolved exact version
|
||||
const lockfile = fs.readFileSync(path.join(opts.dir, 'pnpm-lock.env.yaml'), 'utf8')
|
||||
expect(lockfile).toContain('9.0.0')
|
||||
})
|
||||
|
||||
test('should fall back to ^version when complex range cannot accommodate the new version', async () => {
|
||||
const opts = prepare({
|
||||
devEngines: {
|
||||
packageManager: { name: 'pnpm', version: '>=8.0.0 <9.0.0' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
nock(opts.registries.default)
|
||||
.persist()
|
||||
.get('/pnpm')
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default))
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
await selfUpdate.handler({
|
||||
...opts,
|
||||
managePackageManagerVersions: true,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '>=8.0.0 <9.0.0',
|
||||
},
|
||||
}, [])
|
||||
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
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: {
|
||||
packageManager: { name: 'pnpm', version: '^8' },
|
||||
},
|
||||
})
|
||||
const pkgJsonPath = path.join(opts.dir, 'package.json')
|
||||
nock(opts.registries.default)
|
||||
.persist()
|
||||
.get('/pnpm')
|
||||
.reply(200, createMetadata('9.0.0', opts.registries.default))
|
||||
mockExeMetadata(opts.registries.default, '9.0.0')
|
||||
|
||||
const output = await selfUpdate.handler({
|
||||
...opts,
|
||||
managePackageManagerVersions: true,
|
||||
wantedPackageManager: {
|
||||
name: 'pnpm',
|
||||
version: '^8',
|
||||
},
|
||||
}, [])
|
||||
|
||||
expect(output).toBe('The current project has been updated to use pnpm v9.0.0')
|
||||
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
// Range operator preserved, version updated
|
||||
expect(pkgJson.devEngines.packageManager.version).toBe('^9.0.0')
|
||||
})
|
||||
|
||||
test('self-update finds pnpm that is already in the global dir', async () => {
|
||||
const opts = prepare()
|
||||
const globalDir = opts.globalPkgDir
|
||||
@@ -337,13 +480,8 @@ test('self-update updates the packageManager field in package.json', async () =>
|
||||
},
|
||||
}
|
||||
nock(opts.registries.default)
|
||||
.persist()
|
||||
.get('/pnpm')
|
||||
.reply(200, createMetadata('9.1.0', opts.registries.default))
|
||||
mockExeMetadata(opts.registries.default, '9.1.0')
|
||||
nock(opts.registries.default)
|
||||
.get('/pnpm/-/pnpm-9.1.0.tgz')
|
||||
.replyWithFile(200, pnpmTarballPath)
|
||||
|
||||
const output = await selfUpdate.handler(opts, [])
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
{
|
||||
"path": "../../pkg-manifest/read-project-manifest"
|
||||
},
|
||||
{
|
||||
"path": "../../resolving/npm-resolver"
|
||||
},
|
||||
{
|
||||
"path": "../../store/package-store"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user