feat: add pnpm with <version|current> command (#11275)

## Summary

- **New command `pnpm with <version|current> <args...>`** — runs pnpm at a specific version (or the currently active one) for a single invocation, bypassing the project's `packageManager` and `devEngines.packageManager` pins. Uses the same install mechanism as `pnpm self-update`, caching the downloaded pnpm in the global virtual store for reuse.
- **New config setting `pmOnFail`** — overrides the `onFail` behavior of both `packageManager` and `devEngines.packageManager`. Accepted values: `download`, `error`, `warn`, `ignore`. Readable from CLI flag, env var, `pnpm-workspace.yaml`, or `.npmrc` — useful when version management is handled by an external tool (asdf, mise, Volta, etc.) and the project wants pnpm itself to skip the check.

```
pnpm with current install                    # one-shot, use running pnpm
pnpm with 11.0.0-rc.1 install                # one-shot, use specific version
pnpm install --pm-on-fail=ignore             # direct CLI flag
pnpm install --config.pm-on-fail=ignore      # equivalent via --config.* sugar
pnpm_config_pm_on_fail=ignore pnpm install   # env var
# or in pnpm-workspace.yaml: pmOnFail: ignore
```

## Implementation notes

- Command handler lives in `@pnpm/engine.pm.commands` (next to `self-update` and `setup`).
- `'with'` added to `SPECIALLY_ESCAPED_CMDS` in `cli/parse-cli-args` so args after `<spec>` pass through opaquely like `dlx`/`run`.
- `pnpm with current <cmd> [args]` is rewritten in `pnpm/src/parseCliArgs.ts` to an in-process dispatch — argv is rebuilt in place so any global flags the user put before `with` (e.g. `--dir`, `--filter`) are preserved. `process.env.pnpm_config_pm_on_fail=ignore` is set so the override survives `parseCliArgsLib`'s `-v` / `--help` short-circuits (which discard other parsed options).
- `main.ts` treats `skipPackageManagerCheck: true` as bypassing both the auto-download and the warn/error check (previously only the check). Also skips when `cmd='help'` and the help target is itself a skip-check command, so `pnpm with -h` works in pinned projects without downloading the pinned version first.
- Errors reported to stderr for `with` (aligned with `dlx`/`create`/`sbom`).
- `pmOnFail` wired in `config/reader/src/index.ts`: added to `types`, `Config`, and `pnpmConfigFileKeys`; applied as an override in the `onFail` resolution block.
- The `with <version>` child process sets both `COREPACK_ROOT` (honored by every pnpm release via `isExecutedByCorepack()`) and `pnpm_config_pm_on_fail=ignore` (principled override on new releases that ship the setting). This gives graceful behavior when `pnpm with 9.3.0 install` spawns an older pnpm that predates the new setting.
- Store controller lifecycle in the handler wrapped in `try/finally` to prevent leaks on install errors. Signal-induced child exits return a non-zero exit code so interrupted runs aren't masked as success.
This commit is contained in:
Zoltan Kochan
2026-04-16 22:34:34 +02:00
committed by GitHub
parent 1f82123a80
commit 9af708a613
17 changed files with 421 additions and 40 deletions

View File

@@ -0,0 +1,25 @@
---
"@pnpm/cli.parse-cli-args": minor
"@pnpm/config.reader": minor
"@pnpm/engine.pm.commands": minor
"pnpm": minor
---
Add `pnpm with <version|current> <args...>` command. Runs pnpm at a specific version (or the currently active one) for a single invocation, bypassing the project's `packageManager` and `devEngines.packageManager` pins. Uses the same install mechanism as `pnpm self-update`, caching the downloaded pnpm in the global virtual store for reuse.
Examples:
```
pnpm with current install # ignore the pinned version, use the running pnpm
pnpm with 11.0.0-rc.1 install # install using pnpm 11.0.0-rc.1
pnpm with next install # install using the "next" dist-tag
```
Also adds a new `pmOnFail` setting that overrides the `onFail` behavior of `packageManager` and `devEngines.packageManager`. Accepted values: `download`, `error`, `warn`, `ignore`. Can be set via CLI flag, env var, `pnpm-workspace.yaml`, or `.npmrc` — useful when version management is handled by an external tool (asdf, mise, Volta, etc.) and the project wants pnpm itself to skip the check.
```
pnpm install --pm-on-fail=ignore # direct CLI flag
pnpm_config_pm_on_fail=ignore pnpm install # env var
# or in pnpm-workspace.yaml:
# pmOnFail: ignore
```

View File

@@ -4,7 +4,7 @@ import { findWorkspaceDir } from '@pnpm/workspace.root-finder'
import didYouMean, { ReturnTypeEnums } from 'didyoumean2'
const RECURSIVE_CMDS = new Set(['recursive', 'multi', 'm'])
const SPECIALLY_ESCAPED_CMDS = new Set(['run', 'dlx'])
const SPECIALLY_ESCAPED_CMDS = new Set(['run', 'dlx', 'with'])
export interface ParsedCliArgs {
argv: {

View File

@@ -234,6 +234,7 @@ export interface Config extends OptionsFromRootManifest {
lockfile?: boolean
dedupeInjectedDeps?: boolean
nodeOptions?: string
pmOnFail?: 'download' | 'error' | 'warn' | 'ignore'
packageManagerStrict?: boolean
packageManagerStrictVersion?: boolean
virtualStoreDirMaxLength: number

View File

@@ -103,6 +103,7 @@ export const excludedPnpmKeys = [
'pack-gzip-level',
'patches-dir',
'pnpmfile',
'pm-on-fail',
'package-manager-strict',
'package-manager-strict-version',
'prefer-workspace-packages',

View File

@@ -617,17 +617,24 @@ 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'
// The `pmOnFail` config setting overrides whatever onFail the
// wantedPackageManager carried, so users (and internal callers) can force
// a specific behavior without editing the manifest.
// Otherwise, for the legacy packageManager field, derive onFail from config
// settings. devEngines.packageManager already has onFail set during parsing.
if (pnpmConfig.wantedPackageManager) {
if (pnpmConfig.pmOnFail) {
pnpmConfig.wantedPackageManager.onFail = pnpmConfig.pmOnFail
} else if (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'
}
}
}

View File

@@ -84,6 +84,7 @@ export const pnpmTypes = {
'package-import-method': ['auto', 'hardlink', 'clone', 'copy'],
'patches-dir': String,
pnpmfile: String,
'pm-on-fail': ['download', 'error', 'warn', 'ignore'],
'package-manager-strict': Boolean,
'package-manager-strict-version': Boolean,
'prefer-frozen-lockfile': Boolean,

View File

@@ -46,10 +46,12 @@
"@pnpm/lockfile.types": "workspace:*",
"@pnpm/os.env.path-extender": "catalog:",
"@pnpm/resolving.npm-resolver": "workspace:*",
"@pnpm/shell.path": "workspace:*",
"@pnpm/store.connection-manager": "workspace:*",
"@pnpm/store.controller": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/workspace.project-manifest-reader": "workspace:*",
"cross-spawn": "catalog:",
"path-name": "catalog:",
"ramda": "catalog:",
"render-help": "catalog:",
@@ -66,12 +68,10 @@
"@pnpm/error": "workspace:*",
"@pnpm/logger": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/shell.path": "workspace:*",
"@pnpm/testing.mock-agent": "workspace:*",
"@types/cross-spawn": "catalog:",
"@types/ramda": "catalog:",
"@types/semver": "catalog:",
"cross-spawn": "catalog:"
"@types/semver": "catalog:"
},
"engines": {
"node": ">=22.13"

View File

@@ -1,3 +1,4 @@
export { selfUpdate } from './self-updater/index.js'
export { installPnpm, installPnpmToStore, linkExePlatformBinary } from './self-updater/installPnpm.js'
export { setup } from './setup/index.js'
export { withCmd } from './with/index.js'

View File

@@ -0,0 +1,3 @@
import * as withCmd from './with.js'
export { withCmd }

View File

@@ -0,0 +1,120 @@
import fs from 'node:fs'
import path from 'node:path'
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 { PnpmError } from '@pnpm/error'
import { resolvePackageManagerIntegrities } from '@pnpm/installing.env-installer'
import { prependDirsToPath } from '@pnpm/shell.path'
import { createStoreController, type CreateStoreControllerOptions } from '@pnpm/store.connection-manager'
import crossSpawn from 'cross-spawn'
import { pick } from 'ramda'
import { renderHelp } from 'render-help'
import { installPnpmToStore } from '../self-updater/installPnpm.js'
export const commandNames = ['with']
export const skipPackageManagerCheck = true
export const rcOptionsTypes = cliOptionsTypes
export function cliOptionsTypes (): Record<string, unknown> {
return pick([], allTypes)
}
export function help (): string {
return renderHelp({
description: 'Run pnpm with a specific version (or the currently running one), ignoring the "packageManager" and "devEngines.packageManager" fields of the project manifest.',
descriptionLists: [],
url: docsUrl('with'),
usages: [
'pnpm with current <pnpm args>',
'pnpm with <version> <pnpm args>',
'pnpm with next install',
'pnpm with 10 install',
],
})
}
export type WithCommandOptions = CreateStoreControllerOptions & Pick<Config,
| 'dir'
| 'lockfileDir'
| 'pnpmHomeDir'
| 'virtualStoreDirMaxLength'
> & Pick<ConfigContext,
| 'rootProjectManifestDir'
>
export async function handler (
opts: WithCommandOptions,
params: string[]
): Promise<{ exitCode: number }> {
if (params.length === 0) {
throw new PnpmError('MISSING_WITH_SPEC', 'Missing version argument. Usage: pnpm with <version|current> <args...>')
}
if (isExecutedByCorepack()) {
throw new PnpmError('CANT_USE_WITH_IN_COREPACK', 'The "pnpm with" command does not work under corepack')
}
// `with current` is handled earlier in parseCliArgs.ts, which re-parses it
// for in-process execution, so this handler only ever sees version/dist-tag specs.
const [spec, ...args] = params
fs.mkdirSync(opts.pnpmHomeDir, { recursive: true })
const store = await createStoreController(opts)
let binDir: string
try {
// resolvePackageManagerIntegrities resolves ranges/dist-tags via the
// registry and writes the resolved exact version to the envLockfile.
const envLockfile = await resolvePackageManagerIntegrities(spec, {
rootDir: opts.pnpmHomeDir,
registries: opts.registries,
storeController: store.ctrl,
storeDir: store.dir,
})
const resolvedVersion = envLockfile.importers['.'].packageManagerDependencies?.['pnpm']?.version
if (!resolvedVersion) {
throw new PnpmError('CANNOT_RESOLVE_PNPM', `Cannot resolve pnpm version for "${spec}"`)
}
;({ binDir } = await installPnpmToStore(resolvedVersion, {
envLockfile,
storeController: store.ctrl,
storeDir: store.dir,
registries: opts.registries,
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
packageManager: { name: packageManager.name, version: packageManager.version },
}))
} finally {
await store.ctrl.close()
}
// The child pnpm must skip the packageManager/devEngines check so the requested
// version stays active. Two keys are set for backward compatibility:
// - `COREPACK_ROOT` is honored by every pnpm release that supports corepack
// (older versions skip the pm check whenever this is set).
// - `pnpm_config_pm_on_fail=ignore` is the principled override recognized
// by pnpm releases that ship the `pmOnFail` setting.
const pnpmEnv = prependDirsToPath([binDir])
const spawnEnv: NodeJS.ProcessEnv = {
...process.env,
[pnpmEnv.name]: pnpmEnv.value,
COREPACK_ROOT: process.env.COREPACK_ROOT ?? 'pnpm-with',
pnpm_config_pm_on_fail: 'ignore',
}
const pnpmBinPath = path.join(binDir, 'pnpm')
const { status, signal, error } = crossSpawn.sync(pnpmBinPath, args, {
stdio: 'inherit',
env: spawnEnv,
})
if (error) throw error
if (signal) {
// Best-effort: try to terminate with the same signal the child received.
// If the signal is handled or ignored, fall back to a non-zero exit code
// so the caller doesn't mistake an interrupted run for a successful one.
process.kill(process.pid, signal)
return { exitCode: 1 }
}
return { exitCode: status ?? 0 }
}

12
pnpm-lock.yaml generated
View File

@@ -3615,6 +3615,9 @@ importers:
'@pnpm/resolving.npm-resolver':
specifier: workspace:*
version: link:../../../resolving/npm-resolver
'@pnpm/shell.path':
specifier: workspace:*
version: link:../../../shell/path
'@pnpm/store.connection-manager':
specifier: workspace:*
version: link:../../../store/connection-manager
@@ -3627,6 +3630,9 @@ importers:
'@pnpm/workspace.project-manifest-reader':
specifier: workspace:*
version: link:../../../workspace/project-manifest-reader
cross-spawn:
specifier: 'catalog:'
version: 7.0.6
path-name:
specifier: 'catalog:'
version: 1.0.0
@@ -3658,9 +3664,6 @@ importers:
'@pnpm/prepare':
specifier: workspace:*
version: link:../../../__utils__/prepare
'@pnpm/shell.path':
specifier: workspace:*
version: link:../../../shell/path
'@pnpm/testing.mock-agent':
specifier: workspace:*
version: link:../../../testing/mock-agent
@@ -3673,9 +3676,6 @@ importers:
'@types/semver':
specifier: 'catalog:'
version: 7.7.1
cross-spawn:
specifier: 'catalog:'
version: 7.0.6
engine/runtime/bun-resolver:
dependencies:

View File

@@ -7,7 +7,7 @@ import { config, getCommand, setCommand } from '@pnpm/config.commands'
import { types as allTypes } from '@pnpm/config.reader'
import { audit, licenses, sbom } from '@pnpm/deps.compliance.commands'
import { docs, list, ll, outdated, peers, view, why } from '@pnpm/deps.inspection.commands'
import { selfUpdate, setup } from '@pnpm/engine.pm.commands'
import { selfUpdate, setup, withCmd } from '@pnpm/engine.pm.commands'
import { env, runtime } from '@pnpm/engine.runtime.commands'
import {
create,
@@ -56,6 +56,7 @@ export const GLOBAL_OPTIONS = pick([
'yes',
'include-workspace-root',
'fail-if-no-match',
'pm-on-fail',
], allTypes)
export type CommandResponse = string | { output?: string, exitCode: number }
@@ -183,6 +184,7 @@ const commands: CommandDefinition[] = [
version,
view,
why,
withCmd,
createHelp(helpByCommandName),
...notImplementedCommandDefinitions,
]

View File

@@ -102,11 +102,11 @@ export async function main (inputArgv: string[]): Promise<void> {
workspaceDir,
ignoreNonAuthSettingsFromLocal: isDlxOrCreateCommand,
}) as { config: typeof config, context: ConfigContext })
if (!isExecutedByCorepack() && cmd !== 'setup' && context.wantedPackageManager != null) {
if (!isExecutedByCorepack() && cmd !== 'setup' && context.wantedPackageManager != null && !shouldSkipPmHandling(cmd, cliParams)) {
const pm = context.wantedPackageManager
if (pm.onFail === 'download' && pm.name === 'pnpm' && cmd !== 'self-update') {
if (pm.onFail === 'download' && pm.name === 'pnpm') {
await switchCliVersion(config, context)
} else if (pm.onFail !== 'ignore' && (!cmd || !skipPackageManagerCheckForCommand.has(cmd))) {
} else if (pm.onFail !== 'ignore') {
if (cliOptions.global) {
globalWarn('Using --global skips the package manager check for this project')
} else {
@@ -115,7 +115,7 @@ export async function main (inputArgv: string[]): Promise<void> {
}
}
;({ config, context } = await installConfigDepsAndLoadHooks(config, context) as { config: typeof config, context: ConfigContext })
if (isDlxOrCreateCommand || cmd === 'sbom') {
if (isDlxOrCreateCommand || cmd === 'sbom' || cmd === 'with') {
config.useStderr = true
}
config.argv = argv
@@ -370,6 +370,22 @@ function printError (message: string, hint?: string): void {
}
}
/**
* Whether to skip the packageManager/devEngines handling block (both auto
* download and warn/error check). Returns true when the command itself
* opts out via `skipPackageManagerCheck: true`, or when the user is asking
* for help on such a command — `pnpm help <skippable>` and
* `pnpm <skippable> --help` (which parse-cli-args rewrites to the same
* cmd='help' form) shouldn't download an older pinned pnpm just to render
* help for a command that older pnpm may not even have.
*/
function shouldSkipPmHandling (cmd: string | null, cliParams: string[]): boolean {
if (cmd == null) return false
if (skipPackageManagerCheckForCommand.has(cmd)) return true
if (cmd === 'help' && cliParams[0] != null && skipPackageManagerCheckForCommand.has(cliParams[0])) return true
return false
}
function checkPackageManager (pm: EngineDependency): void {
if (!pm.name) return
const shouldError = pm.onFail === 'error' || pm.onFail === 'download'

View File

@@ -1,4 +1,5 @@
import { parseCliArgs as parseCliArgsLib, type ParsedCliArgs } from '@pnpm/cli.parse-cli-args'
import { PnpmError } from '@pnpm/error'
import {
getCliOptionsTypes,
@@ -20,7 +21,7 @@ export async function parseCliArgs (inputArgv: string[]): Promise<ParsedCliArgsW
if (builtInCommandForced) {
inputArgv.splice(0, 1)
}
const result = await parseCliArgsLib({
const libOpts = {
fallbackCommand: 'run',
escapeArgs: ['create', 'exec', 'test'],
getCommandLongName: getCommandFullName,
@@ -29,6 +30,47 @@ export async function parseCliArgs (inputArgv: string[]): Promise<ParsedCliArgsW
shorthandsByCommandName,
universalOptionsTypes: GLOBAL_OPTIONS,
universalShorthands,
}, inputArgv)
}
let result = await parseCliArgsLib(libOpts, inputArgv)
// `pnpm [global-opts] with current <cmd> [args]` is sugar for
// `pnpm [global-opts] --pm-on-fail=ignore <cmd> [args]` — re-parse so the
// inner command is dispatched directly, in-process. The override is
// propagated via env var (not --pm-on-fail=ignore in argv) so it survives
// parseCliArgsLib's special short-circuits like the -v/--version
// interceptor, which discards other parsed options.
//
// We rebuild argv by removing the `with current` tokens in place so that
// any global flags the user put BEFORE `with` (e.g. `--dir`, `--filter`)
// are preserved.
if (result.cmd === 'with' && result.params[0] === 'current') {
const withIdx = findWithCurrentIndex(inputArgv)
if (withIdx < 0 || inputArgv.length - withIdx - 2 === 0) {
throw new PnpmError('MISSING_WITH_CURRENT_CMD',
'Missing command after "current". Usage: pnpm with current <command> [args...]')
}
process.env.pnpm_config_pm_on_fail = 'ignore'
result = await parseCliArgsLib(libOpts, [
...inputArgv.slice(0, withIdx),
...inputArgv.slice(withIdx + 2),
])
}
return { ...result, builtInCommandForced }
}
/**
* Locate the `with current` token pair in argv. We assume the first
* occurrence that's plausibly the command (not the value of a preceding flag)
* is the one. Good enough for realistic CLI usage — no pnpm option is
* expected to take the literal value `with`.
*/
function findWithCurrentIndex (argv: string[]): number {
for (let i = 0; i < argv.length - 1; i++) {
if (argv[i] !== 'with' || argv[i + 1] !== 'current') continue
const prev = argv[i - 1]
// If the previous token is a long flag without an `=value` form, it may
// be consuming `with` as its value — skip this occurrence in that case.
if (prev != null && prev.startsWith('--') && !prev.includes('=')) continue
return i
}
return -1
}

View File

@@ -34,7 +34,7 @@ export async function switchCliVersion (config: Config, context: ConfigContext):
pmVersion = envLockfile.importers['.'].packageManagerDependencies?.['pnpm']?.version
if (!pmVersion) {
globalWarn(`Cannot resolve pnpm version for "${pm.version}"`)
await storeToUse?.ctrl.close()
await storeToUse.ctrl.close()
return
}
} else if (!isPackageManagerResolved(envLockfile, pmVersion)) {
@@ -48,7 +48,8 @@ export async function switchCliVersion (config: Config, context: ConfigContext):
})
}
// If the wanted version matches the current version, no switch needed
// If the wanted version matches the current version, no switch needed.
// Skip install-to-store entirely — we're already running this version.
if (pmVersion === packageManager.version) {
await storeToUse?.ctrl.close()
return
@@ -61,19 +62,23 @@ export async function switchCliVersion (config: Config, context: ConfigContext):
}
if (!envLockfile) {
await storeToUse.ctrl.close()
throw new PnpmError('NO_PKG_MANAGER_INTEGRITY', `The packageManager dependency ${pmVersion} was not found in pnpm-lock.yaml`)
}
const { binDir: wantedPnpmBinDir } = await installPnpmToStore(pmVersion, {
envLockfile,
storeController: storeToUse.ctrl,
storeDir: storeToUse.dir,
registries: config.registries,
virtualStoreDirMaxLength: config.virtualStoreDirMaxLength,
packageManager: { name: packageManager.name, version: packageManager.version },
})
await storeToUse.ctrl.close()
let wantedPnpmBinDir: string
try {
;({ binDir: wantedPnpmBinDir } = await installPnpmToStore(pmVersion, {
envLockfile,
storeController: storeToUse.ctrl,
storeDir: storeToUse.dir,
registries: config.registries,
virtualStoreDirMaxLength: config.virtualStoreDirMaxLength,
packageManager: { name: packageManager.name, version: packageManager.version },
}))
} finally {
await storeToUse.ctrl.close()
}
const pnpmEnv = prependDirsToPath([wantedPnpmBinDir])
if (!pnpmEnv.updated) {

View File

@@ -1,4 +1,5 @@
import { prepare } from '@pnpm/prepare'
import { writeYamlFileSync } from 'write-yaml-file'
import { execPnpmSync } from './utils/index.js'
@@ -245,3 +246,57 @@ test('devEngines.packageManager takes precedence over packageManager field', asy
expect(stderr.toString()).toContain('This project is configured to use 0.0.1 of pnpm')
expect(stderr.toString()).toContain('"packageManager" will be ignored')
})
test('pmOnFail=ignore via env var bypasses the devEngines.packageManager check', async () => {
prepare({
devEngines: {
packageManager: {
name: 'pnpm',
version: '0.0.1',
onFail: 'error',
},
},
})
const { status, stderr } = execPnpmSync(['install'], {
env: { pnpm_config_pm_on_fail: 'ignore' },
})
expect(status).toBe(0)
expect(stderr.toString()).not.toContain('0.0.1')
})
test('pmOnFail via --pm-on-fail CLI flag bypasses the devEngines.packageManager check', async () => {
prepare({
devEngines: {
packageManager: {
name: 'pnpm',
version: '0.0.1',
onFail: 'error',
},
},
})
expect(execPnpmSync(['install', '--pm-on-fail=ignore']).status).toBe(0)
expect(execPnpmSync(['install', '--config.pm-on-fail=ignore']).status).toBe(0)
})
test('pmOnFail=ignore set in pnpm-workspace.yaml bypasses the devEngines.packageManager check', async () => {
prepare({
devEngines: {
packageManager: {
name: 'pnpm',
version: '0.0.1',
onFail: 'error',
},
},
})
writeYamlFileSync('pnpm-workspace.yaml', {
pmOnFail: 'ignore',
})
const { status, stderr } = execPnpmSync(['install'])
expect(status).toBe(0)
expect(stderr.toString()).not.toContain('0.0.1')
})

View File

@@ -0,0 +1,102 @@
import path from 'node:path'
import { prepare } from '@pnpm/prepare'
import { writeJsonFileSync } from 'write-json-file'
import { execPnpmSync } from './utils/index.js'
test('pnpm with current runs the currently active pnpm even when the project pins a different version', () => {
prepare()
const pnpmHome = path.resolve('pnpm')
const env = { PNPM_HOME: pnpmHome }
writeJsonFileSync('package.json', {
packageManager: 'pnpm@9.3.0',
})
const { status, stdout } = execPnpmSync(['with', 'current', 'help'], { env })
expect(status).toBe(0)
expect(stdout.toString()).not.toContain('Version 9.3.0')
})
test('pnpm with current bypasses the packageManager check when an unrelated package manager is pinned', () => {
prepare()
const pnpmHome = path.resolve('pnpm')
const env = { PNPM_HOME: pnpmHome }
writeJsonFileSync('package.json', {
packageManager: 'yarn@4.0.0',
})
const { status, stderr } = execPnpmSync(['with', 'current', 'help'], { env })
expect(status).toBe(0)
expect(stderr.toString()).not.toContain('This project is configured to use yarn')
})
test('pnpm with current bypasses devEngines.packageManager with onFail=download', () => {
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 { status, stdout } = execPnpmSync(['with', 'current', 'help'], { env })
expect(status).toBe(0)
expect(stdout.toString()).not.toContain('Version 9.3.0')
})
test('pnpm with forwards subsequent args to the child pnpm', () => {
prepare()
writeJsonFileSync('package.json', {
name: 'project',
version: '1.0.0',
})
const { status, stdout } = execPnpmSync(['with', 'current', '--version'])
expect(status).toBe(0)
expect(stdout.toString().trim()).toMatch(/^\d+\.\d+\.\d+/)
})
test('pnpm with fails when no spec is provided', () => {
prepare()
const { status, stderr } = execPnpmSync(['with'])
expect(status).not.toBe(0)
expect(stderr.toString()).toContain('Missing version argument')
})
test('pnpm with <version> downloads and runs the specified pnpm version', () => {
prepare()
const pnpmHome = path.resolve('pnpm')
const env = { PNPM_HOME: pnpmHome }
const { status, stdout } = execPnpmSync(['with', '9.3.0', 'help'], { env })
expect(status).toBe(0)
expect(stdout.toString()).toContain('Version 9.3.0')
})
test('pnpm with <version> ignores the packageManager pin and uses the requested version', () => {
prepare()
const pnpmHome = path.resolve('pnpm')
const env = { PNPM_HOME: pnpmHome }
writeJsonFileSync('package.json', {
packageManager: 'pnpm@9.1.0',
})
const { status, stdout } = execPnpmSync(['with', '9.3.0', 'help'], { env })
expect(status).toBe(0)
expect(stdout.toString()).toContain('Version 9.3.0')
expect(stdout.toString()).not.toContain('Version 9.1.0')
})