mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-01 12:41:16 -04:00
fix: validate devEngines runtime onFail (#11822)
Fixes #11818 ## Summary `devEngines.runtime` / `engines.runtime` entries with `onFail: error` or `warn` silently did nothing — only `onFail: download` had any effect. This PR wires up validation for all three supported runtimes (node, deno, bun). - Add `getSystemDenoVersion` / `getSystemBunVersion` and a generic `getSystemRuntimeVersion(name)` dispatcher in the runtime-version helper package. - Walk each runtime entry in the manifest during pnpm startup, compare to the live system runtime, and throw `ERR_PNPM_BAD_RUNTIME_VERSION` (or warn) on a mismatch. Invalid ranges (e.g. `"invalid range"`) are reported instead of crashing `semver.minVersion`. Missing runtimes ("no Node.js on the system") get the same error path. - The shell-out for deno/bun only runs when the manifest configures them AND `onFail` is `error`/`warn`. `download`/`ignore` short-circuit, and projects with no runtime pin pay nothing. Memoized per runtime. - `pnpm --version`, `pnpm --help`, and `pnpm <cmd> --global` are exempt from the check. - Rename `@pnpm/engine.runtime.system-node-version` → `@pnpm/engine.runtime.system-version` to match its broader scope; hoist `RuntimeName` / `RUNTIME_NAMES` / `isRuntimeAlias` to `@pnpm/types` so callers don't need to depend on `pkg-manifest.utils` just for the alias check. ## Tests - `pnpm --filter pnpm run compile` - `pnpm --filter pnpm exec jest packageManagerCheck.test` — 42 passing. New coverage: node/deno/bun version mismatch, invalid range, missing range, multi-entry runtime arrays, `engines.runtime` path (not just `devEngines.runtime`), and the `pnpm --version` exemption. - `pnpm --filter @pnpm/engine.runtime.system-version test` — 10 passing, 100% statement coverage; unit tests for each helper and the dispatcher. - Manual end-to-end smoke tests against the rebuilt bundle for deno and bun version mismatch. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added runtime version validation for Node.js, Deno, and Bun. The system now enforces `devEngines.runtime` and `engines.runtime` declarations with configurable failure behavior (`error`, `warn`, or `ignore`). * Enhanced error messages for runtime version mismatches with helpful suggestions for overrides. * **Improvements** * Improved system runtime detection and version checking across multiple runtime environments. --------- Co-authored-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
8
.changeset/runtime-onfail-node-check.md
Normal file
8
.changeset/runtime-onfail-node-check.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@pnpm/engine.runtime.system-version": minor
|
||||
"@pnpm/types": minor
|
||||
"@pnpm/config.reader": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Validate `devEngines.runtime` and `engines.runtime` version ranges for `node`, `deno`, and `bun` when `onFail` is set to `error` or `warn`. Previously these settings only had an effect with `onFail: 'download'` — the `error` and `warn` modes silently did nothing [#11818](https://github.com/pnpm/pnpm/issues/11818). Violations now throw `ERR_PNPM_BAD_RUNTIME_VERSION`.
|
||||
@@ -34,7 +34,7 @@
|
||||
"dependencies": {
|
||||
"@pnpm/cli.meta": "workspace:*",
|
||||
"@pnpm/core-loggers": "workspace:*",
|
||||
"@pnpm/engine.runtime.system-node-version": "workspace:*",
|
||||
"@pnpm/engine.runtime.system-version": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"detect-libc": "catalog:",
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
installCheckLogger,
|
||||
skippedOptionalDependencyLogger,
|
||||
} from '@pnpm/core-loggers'
|
||||
import { getSystemNodeVersion } from '@pnpm/engine.runtime.system-node-version'
|
||||
import { getSystemNodeVersion } from '@pnpm/engine.runtime.system-version'
|
||||
import type { SupportedArchitectures } from '@pnpm/types'
|
||||
|
||||
import { checkEngine, UnsupportedEngineError, type WantedEngine } from './checkEngine.js'
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"path": "../../core/types"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/runtime/system-node-version"
|
||||
"path": "../../engine/runtime/system-version"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@ function getNodeVersionFromEnginesRuntime (manifest: ProjectManifest): string |
|
||||
const runtimes: EngineDependency[] = Array.isArray(enginesRuntime) ? enginesRuntime : [enginesRuntime]
|
||||
const nodeRuntime = runtimes.find((r) => r.name === 'node')
|
||||
if (nodeRuntime?.version == null) continue
|
||||
if (!semver.validRange(nodeRuntime.version)) continue
|
||||
const minVersion = semver.minVersion(nodeRuntime.version)
|
||||
if (minVersion != null) {
|
||||
return minVersion.version
|
||||
|
||||
@@ -53,6 +53,14 @@ export interface DependenciesMeta {
|
||||
}
|
||||
}
|
||||
|
||||
export const RUNTIME_NAMES = ['node', 'deno', 'bun'] as const
|
||||
|
||||
export type RuntimeName = typeof RUNTIME_NAMES[number]
|
||||
|
||||
export function isRuntimeAlias (alias: string): alias is RuntimeName {
|
||||
return (RUNTIME_NAMES as readonly string[]).includes(alias)
|
||||
}
|
||||
|
||||
export interface EngineDependency {
|
||||
name: string
|
||||
version?: string
|
||||
|
||||
2
deps/graph-hasher/package.json
vendored
2
deps/graph-hasher/package.json
vendored
@@ -33,7 +33,7 @@
|
||||
"dependencies": {
|
||||
"@pnpm/crypto.object-hasher": "workspace:*",
|
||||
"@pnpm/deps.path": "workspace:*",
|
||||
"@pnpm/engine.runtime.system-node-version": "workspace:*",
|
||||
"@pnpm/engine.runtime.system-version": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/lockfile.utils": "workspace:*",
|
||||
"@pnpm/resolving.resolver-base": "workspace:*",
|
||||
|
||||
2
deps/graph-hasher/src/index.ts
vendored
2
deps/graph-hasher/src/index.ts
vendored
@@ -1,6 +1,6 @@
|
||||
import { hashObject, hashObjectWithoutSorting } from '@pnpm/crypto.object-hasher'
|
||||
import { getPkgIdWithPatchHash, refToRelative } from '@pnpm/deps.path'
|
||||
import { engineName } from '@pnpm/engine.runtime.system-node-version'
|
||||
import { engineName } from '@pnpm/engine.runtime.system-version'
|
||||
import type { LockfileObject, LockfileResolution, PackageSnapshot } from '@pnpm/lockfile.types'
|
||||
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils'
|
||||
import { resolvePlatformSelector, selectPlatformVariant } from '@pnpm/resolving.resolver-base'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from '@jest/globals'
|
||||
import { hashObject, hashObjectWithoutSorting } from '@pnpm/crypto.object-hasher'
|
||||
import { calcGraphNodeHash, type DepsGraph, type DepsStateCache, type PkgMeta } from '@pnpm/deps.graph-hasher'
|
||||
import { engineName } from '@pnpm/engine.runtime.system-node-version'
|
||||
import { engineName } from '@pnpm/engine.runtime.system-version'
|
||||
import type { DepPath, PkgIdWithPatchHash } from '@pnpm/types'
|
||||
|
||||
// Track the same script-runner-Node value the production code uses
|
||||
|
||||
2
deps/graph-hasher/test/index.ts
vendored
2
deps/graph-hasher/test/index.ts
vendored
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from '@jest/globals'
|
||||
import { hashObject, hashObjectWithoutSorting } from '@pnpm/crypto.object-hasher'
|
||||
import { calcDepState, calcGraphNodeHash, findRuntimeNodeVersion, readSnapshotRuntimePin } from '@pnpm/deps.graph-hasher'
|
||||
import { engineName } from '@pnpm/engine.runtime.system-node-version'
|
||||
import { engineName } from '@pnpm/engine.runtime.system-version'
|
||||
import type { DepPath, PkgIdWithPatchHash } from '@pnpm/types'
|
||||
|
||||
// Match the function the production code uses (see
|
||||
|
||||
2
deps/graph-hasher/tsconfig.json
vendored
2
deps/graph-hasher/tsconfig.json
vendored
@@ -16,7 +16,7 @@
|
||||
"path": "../../crypto/object-hasher"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/runtime/system-node-version"
|
||||
"path": "../../engine/runtime/system-version"
|
||||
},
|
||||
{
|
||||
"path": "../../lockfile/types"
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
# @pnpm/env.system-node-version
|
||||
|
||||
## 1100.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli.meta@1100.0.4
|
||||
|
||||
## 1100.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 3ddde2b: **fix**: anchor the side-effects-cache key and global-virtual-store hash to the project's script-runner Node — `engines.runtime` pin when present, shell `node` otherwise — instead of pnpm's own runtime.
|
||||
|
||||
`ENGINE_NAME` (the `<platform>;<arch>;node<major>` prefix used as the side-effects-cache key and the engine portion of the GVS hash) was computed from `process.version` — the Node that runs pnpm itself. That was wrong in two situations:
|
||||
|
||||
1. **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on the user's `PATH` that actually spawns lifecycle scripts. Two pnpm installations on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store across two Node majors even though both installs would run scripts on the same shell `node`.
|
||||
2. **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version via `devEngines.runtime` (pnpm v11+), pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node — so two installs of the same project with two different runner Nodes would still disagree on the GVS slot path even though scripts run on the same pinned Node.
|
||||
|
||||
Three changes:
|
||||
|
||||
- `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)`. Resolves the version in this order: explicit override → `getSystemNodeVersion()` (which already prefers `node --version` over `process.version` in SEA contexts) → `process.version`.
|
||||
- `@pnpm/deps.graph-hasher` now exports `findRuntimeNodeVersion(snapshotKeys)` — scans an iterable of lockfile snapshot keys for a `node@runtime:<version>` entry and returns its bare version string. `calcDepState` and `calcGraphNodeHash`/`iterateHashedGraphNodes` accept a `nodeVersion?` (in the options bag for the first, as a trailing parameter / ctx field for the others), forwarded to `engineName()`. The default (no override) preserves the pre-change behaviour. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working; in non-SEA, non-pinned contexts every value lines up.
|
||||
- Every install-side caller of the graph-hasher (`@pnpm/installing.deps-resolver`, `@pnpm/installing.deps-restorer`, `@pnpm/installing.deps-installer`, `@pnpm/building.during-install`, `@pnpm/building.after-install`, `@pnpm/deps.graph-builder`) now derives the project's pinned runtime via `findRuntimeNodeVersion(Object.keys(graph))` once per invocation and threads it through.
|
||||
|
||||
On upgrade, two one-time GVS slot churns are possible:
|
||||
|
||||
- **SEA-pnpm users** without a runtime pin: slots that previously hashed under the embedded-Node major (e.g. `node26`) now hash under the shell-Node major (e.g. `node24`), matching what pacquet, the npm-published `pnpm` package, and any other pnpm-compatible tool already produce.
|
||||
- **Projects with a `devEngines.runtime` pin**: slots that previously hashed under the runner's Node major now hash under the pinned Node major, matching what the lifecycle scripts will actually run on.
|
||||
|
||||
In both cases the old slots become prune-eligible.
|
||||
|
||||
## 1100.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli.meta@1100.0.3
|
||||
|
||||
## 1100.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 184ce26: Fix the package name in README.md.
|
||||
- Updated dependencies [184ce26]
|
||||
- @pnpm/cli.meta@1100.0.2
|
||||
|
||||
## 1100.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli.meta@1100.0.1
|
||||
|
||||
## 1001.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 491a84f: This package is now pure ESM.
|
||||
- 7d2fd48: Node.js v18, 19, 20, and 21 support discontinued.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [491a84f]
|
||||
- Updated dependencies [7d2fd48]
|
||||
- @pnpm/cli.meta@1001.0.0
|
||||
|
||||
## 1000.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.11
|
||||
|
||||
## 1000.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.10
|
||||
|
||||
## 1000.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.9
|
||||
|
||||
## 1000.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.8
|
||||
|
||||
## 1000.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.7
|
||||
|
||||
## 1000.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.6
|
||||
|
||||
## 1000.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.5
|
||||
|
||||
## 1000.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.4
|
||||
|
||||
## 1000.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.3
|
||||
|
||||
## 1000.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.2
|
||||
|
||||
## 1000.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @pnpm/cli-meta@1000.0.1
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e476b07: Don't crash if the `use-node-version` setting is used and the system has no Node.js installed [#8769](https://github.com/pnpm/pnpm/issues/8769).
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- d04f7f2: Initial release.
|
||||
@@ -1,15 +0,0 @@
|
||||
# @pnpm/engine.runtime.system-node-version
|
||||
|
||||
> Detects the current system node version
|
||||
|
||||
[](https://npmx.dev/package/@pnpm/engine.runtime.system-node-version)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @pnpm/engine.runtime.system-node-version
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
1
engine/runtime/system-version/CHANGELOG.md
Normal file
1
engine/runtime/system-version/CHANGELOG.md
Normal file
@@ -0,0 +1 @@
|
||||
# @pnpm/engine.runtime.system-version
|
||||
15
engine/runtime/system-version/README.md
Normal file
15
engine/runtime/system-version/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# @pnpm/engine.runtime.system-version
|
||||
|
||||
> Detects the current system version of supported runtimes (Node.js, Deno, Bun)
|
||||
|
||||
[](https://npmx.dev/package/@pnpm/engine.runtime.system-version)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @pnpm/engine.runtime.system-version
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@pnpm/engine.runtime.system-node-version",
|
||||
"version": "1100.1.1",
|
||||
"description": "Detects the current system node version",
|
||||
"name": "@pnpm/engine.runtime.system-version",
|
||||
"version": "1100.0.0-0",
|
||||
"description": "Detects the current system version of supported runtimes (Node.js, Deno, Bun)",
|
||||
"keywords": [
|
||||
"pnpm",
|
||||
"pnpm11",
|
||||
@@ -9,8 +9,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"repository": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-node-version",
|
||||
"homepage": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-node-version#readme",
|
||||
"repository": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-version",
|
||||
"homepage": "https://github.com/pnpm/pnpm/tree/main/engine/runtime/system-version#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
@@ -33,12 +33,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/cli.meta": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"execa": "catalog:",
|
||||
"memoize": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "catalog:",
|
||||
"@pnpm/engine.runtime.system-node-version": "workspace:*"
|
||||
"@pnpm/engine.runtime.system-version": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.13"
|
||||
@@ -1,4 +1,5 @@
|
||||
import { detectIfCurrentPkgIsExecutable } from '@pnpm/cli.meta'
|
||||
import type { RuntimeName } from '@pnpm/types'
|
||||
import * as execa from 'execa'
|
||||
import mem from 'memoize'
|
||||
|
||||
@@ -14,7 +15,38 @@ export function getSystemNodeVersionNonCached (): string | undefined {
|
||||
return process.version
|
||||
}
|
||||
|
||||
export function getSystemDenoVersionNonCached (): string | undefined {
|
||||
try {
|
||||
// `deno --version` prints e.g. "deno 1.40.0 (release, ...)\nv8 ..."
|
||||
const output = execa.sync('deno', ['--version']).stdout?.toString() ?? ''
|
||||
const match = /^deno\s+(\d+\.\d+\.\d\S*)/m.exec(output)
|
||||
return match?.[1] ? `v${match[1]}` : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getSystemBunVersionNonCached (): string | undefined {
|
||||
try {
|
||||
// `bun --version` prints just the bare version, e.g. "1.1.0".
|
||||
const output = execa.sync('bun', ['--version']).stdout?.toString().trim() ?? ''
|
||||
return /^\d+\.\d+\.\d+/.test(output) ? `v${output}` : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const getSystemNodeVersion = mem(getSystemNodeVersionNonCached)
|
||||
export const getSystemDenoVersion = mem(getSystemDenoVersionNonCached)
|
||||
export const getSystemBunVersion = mem(getSystemBunVersionNonCached)
|
||||
|
||||
export function getSystemRuntimeVersion (name: RuntimeName): string | undefined {
|
||||
switch (name) {
|
||||
case 'node': return getSystemNodeVersion()
|
||||
case 'deno': return getSystemDenoVersion()
|
||||
case 'bun': return getSystemBunVersion()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `<platform>;<arch>;node<major>` string used as the side-effects
|
||||
@@ -11,7 +11,13 @@ jest.unstable_mockModule('execa', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
const { getSystemNodeVersionNonCached, engineName } = await import('../lib/index.js')
|
||||
const {
|
||||
getSystemNodeVersionNonCached,
|
||||
getSystemDenoVersionNonCached,
|
||||
getSystemBunVersionNonCached,
|
||||
getSystemRuntimeVersion,
|
||||
engineName,
|
||||
} = await import('../lib/index.js')
|
||||
const execa = await import('execa')
|
||||
|
||||
test('getSystemNodeVersion() executed from an executable pnpm CLI', () => {
|
||||
@@ -36,6 +42,54 @@ test('getSystemNodeVersion() returns undefined if execa.sync throws an error', (
|
||||
expect(execa.sync).toHaveBeenCalledWith('node', ['--version'])
|
||||
})
|
||||
|
||||
test('getSystemDenoVersion() parses the first line of `deno --version`', () => {
|
||||
jest.mocked(execa.sync).mockReturnValueOnce({
|
||||
stdout: 'deno 1.40.0 (release, aarch64-apple-darwin)\nv8 12.1.285.27\ntypescript 5.3.3',
|
||||
} as ReturnType<typeof execa.sync>)
|
||||
expect(getSystemDenoVersionNonCached()).toBe('v1.40.0')
|
||||
expect(execa.sync).toHaveBeenCalledWith('deno', ['--version'])
|
||||
})
|
||||
|
||||
test('getSystemDenoVersion() returns undefined when deno is missing or output is unexpected', () => {
|
||||
jest.mocked(execa.sync).mockImplementationOnce(() => {
|
||||
throw new Error('not found: deno')
|
||||
})
|
||||
expect(getSystemDenoVersionNonCached()).toBeUndefined()
|
||||
|
||||
jest.mocked(execa.sync).mockReturnValueOnce({ stdout: 'unexpected output' } as ReturnType<typeof execa.sync>)
|
||||
expect(getSystemDenoVersionNonCached()).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getSystemBunVersion() parses the bare version printed by `bun --version`', () => {
|
||||
jest.mocked(execa.sync).mockReturnValueOnce({ stdout: '1.1.0\n' } as ReturnType<typeof execa.sync>)
|
||||
expect(getSystemBunVersionNonCached()).toBe('v1.1.0')
|
||||
expect(execa.sync).toHaveBeenCalledWith('bun', ['--version'])
|
||||
})
|
||||
|
||||
test('getSystemBunVersion() returns undefined when bun is missing', () => {
|
||||
jest.mocked(execa.sync).mockImplementationOnce(() => {
|
||||
throw new Error('not found: bun')
|
||||
})
|
||||
expect(getSystemBunVersionNonCached()).toBeUndefined()
|
||||
})
|
||||
|
||||
test('getSystemRuntimeVersion() dispatches to the per-runtime helpers', () => {
|
||||
isSea = false
|
||||
expect(getSystemRuntimeVersion('node')).toBe(process.version)
|
||||
|
||||
jest.mocked(execa.sync).mockReturnValueOnce({
|
||||
stdout: 'deno 9.9.9 (release)',
|
||||
} as ReturnType<typeof execa.sync>)
|
||||
expect(getSystemRuntimeVersion('deno')).toBe('v9.9.9')
|
||||
expect(execa.sync).toHaveBeenLastCalledWith('deno', ['--version'])
|
||||
|
||||
jest.mocked(execa.sync).mockReturnValueOnce({
|
||||
stdout: '9.9.9\n',
|
||||
} as ReturnType<typeof execa.sync>)
|
||||
expect(getSystemRuntimeVersion('bun')).toBe('v9.9.9')
|
||||
expect(execa.sync).toHaveBeenLastCalledWith('bun', ['--version'])
|
||||
})
|
||||
|
||||
test('engineName() honours an explicit nodeVersion over the host probe', () => {
|
||||
// The pinned-runtime override path: when a project's
|
||||
// `engines.runtime` / `devEngines.runtime` resolves to a specific
|
||||
@@ -11,6 +11,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../cli/meta"
|
||||
},
|
||||
{
|
||||
"path": "../../../core/types"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "catalog:",
|
||||
"@pnpm/engine.runtime.system-node-version": "workspace:*",
|
||||
"@pnpm/engine.runtime.system-version": "workspace:*",
|
||||
"@pnpm/exec.commands": "workspace:*",
|
||||
"@pnpm/logger": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
|
||||
@@ -9,13 +9,13 @@ import { DLX_DEFAULT_OPTS as DEFAULT_OPTS } from './utils/index.js'
|
||||
const {
|
||||
getSystemNodeVersion: originalGetSystemNodeVersion,
|
||||
engineName: originalEngineName,
|
||||
} = await import('@pnpm/engine.runtime.system-node-version')
|
||||
} = await import('@pnpm/engine.runtime.system-version')
|
||||
// Re-export every public symbol the package surfaces so downstream
|
||||
// dynamic imports (e.g. `@pnpm/deps.graph-hasher`'s use of
|
||||
// `engineName` for the GVS hash) keep working under the mock. Only
|
||||
// `getSystemNodeVersion` is wrapped with `jest.fn` for spy-ability;
|
||||
// `engineName` delegates straight back to the original.
|
||||
jest.unstable_mockModule('@pnpm/engine.runtime.system-node-version', () => ({
|
||||
jest.unstable_mockModule('@pnpm/engine.runtime.system-version', () => ({
|
||||
getSystemNodeVersion: jest.fn(originalGetSystemNodeVersion),
|
||||
engineName: originalEngineName,
|
||||
}))
|
||||
@@ -29,7 +29,7 @@ jest.unstable_mockModule('@pnpm/installing.commands', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const systemNodeVersion = await import('@pnpm/engine.runtime.system-node-version')
|
||||
const systemNodeVersion = await import('@pnpm/engine.runtime.system-version')
|
||||
const { add } = await import('@pnpm/installing.commands')
|
||||
const { dlx } = await import('@pnpm/exec.commands')
|
||||
const { approveBuilds } = await import('@pnpm/building.commands')
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"path": "../../engine/runtime/commands"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/runtime/system-node-version"
|
||||
"path": "../../engine/runtime/system-version"
|
||||
},
|
||||
{
|
||||
"path": "../../installing/client"
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import type {
|
||||
DependenciesField,
|
||||
EngineDependency,
|
||||
ProjectManifest,
|
||||
import {
|
||||
type DependenciesField,
|
||||
type EngineDependency,
|
||||
type ProjectManifest,
|
||||
RUNTIME_NAMES,
|
||||
} from '@pnpm/types'
|
||||
|
||||
export const RUNTIME_NAMES = ['node', 'deno', 'bun'] as const
|
||||
|
||||
export type RuntimeName = typeof RUNTIME_NAMES[number]
|
||||
|
||||
export function isRuntimeAlias (alias: string): alias is RuntimeName {
|
||||
return (RUNTIME_NAMES as readonly string[]).includes(alias)
|
||||
}
|
||||
|
||||
export function convertEnginesRuntimeToDependencies (
|
||||
manifest: ProjectManifest,
|
||||
enginesFieldName: 'devEngines' | 'engines',
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -2592,9 +2592,9 @@ importers:
|
||||
'@pnpm/core-loggers':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/core-loggers
|
||||
'@pnpm/engine.runtime.system-node-version':
|
||||
'@pnpm/engine.runtime.system-version':
|
||||
specifier: workspace:*
|
||||
version: link:../../engine/runtime/system-node-version
|
||||
version: link:../../engine/runtime/system-version
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/error
|
||||
@@ -3375,9 +3375,9 @@ importers:
|
||||
'@pnpm/deps.path':
|
||||
specifier: workspace:*
|
||||
version: link:../path
|
||||
'@pnpm/engine.runtime.system-node-version':
|
||||
'@pnpm/engine.runtime.system-version':
|
||||
specifier: workspace:*
|
||||
version: link:../../engine/runtime/system-node-version
|
||||
version: link:../../engine/runtime/system-version
|
||||
'@pnpm/lockfile.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/types
|
||||
@@ -4229,11 +4229,14 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.1
|
||||
|
||||
engine/runtime/system-node-version:
|
||||
engine/runtime/system-version:
|
||||
dependencies:
|
||||
'@pnpm/cli.meta':
|
||||
specifier: workspace:*
|
||||
version: link:../../../cli/meta
|
||||
'@pnpm/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../../core/types
|
||||
execa:
|
||||
specifier: 'catalog:'
|
||||
version: safe-execa@0.3.0
|
||||
@@ -4244,7 +4247,7 @@ importers:
|
||||
'@jest/globals':
|
||||
specifier: 'catalog:'
|
||||
version: 30.3.0
|
||||
'@pnpm/engine.runtime.system-node-version':
|
||||
'@pnpm/engine.runtime.system-version':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
@@ -4368,9 +4371,9 @@ importers:
|
||||
'@jest/globals':
|
||||
specifier: 'catalog:'
|
||||
version: 30.3.0
|
||||
'@pnpm/engine.runtime.system-node-version':
|
||||
'@pnpm/engine.runtime.system-version':
|
||||
specifier: workspace:*
|
||||
version: link:../../engine/runtime/system-node-version
|
||||
version: link:../../engine/runtime/system-version
|
||||
'@pnpm/exec.commands':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
@@ -7787,6 +7790,9 @@ importers:
|
||||
'@pnpm/engine.runtime.commands':
|
||||
specifier: workspace:*
|
||||
version: link:../engine/runtime/commands
|
||||
'@pnpm/engine.runtime.system-version':
|
||||
specifier: workspace:*
|
||||
version: link:../engine/runtime/system-version
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../core/error
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"@pnpm/deps.path": "workspace:*",
|
||||
"@pnpm/engine.pm.commands": "workspace:*",
|
||||
"@pnpm/engine.runtime.commands": "workspace:*",
|
||||
"@pnpm/engine.runtime.system-version": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/exec.commands": "workspace:*",
|
||||
"@pnpm/hooks.pnpmfile": "workspace:*",
|
||||
|
||||
116
pnpm/src/main.ts
116
pnpm/src/main.ts
@@ -12,9 +12,10 @@ import { stripVTControlCharacters as stripAnsi } from 'node:util'
|
||||
import { isExecutedByCorepack, packageManager } from '@pnpm/cli.meta'
|
||||
import type { Config, ConfigContext } from '@pnpm/config.reader'
|
||||
import { executionTimeLogger, scopeLogger } from '@pnpm/core-loggers'
|
||||
import { getSystemRuntimeVersion } from '@pnpm/engine.runtime.system-version'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { globalWarn, logger } from '@pnpm/logger'
|
||||
import type { EngineDependency } from '@pnpm/types'
|
||||
import { type EngineDependency, isRuntimeAlias, type RuntimeName } from '@pnpm/types'
|
||||
import { finishWorkers } from '@pnpm/worker'
|
||||
import { safeReadProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader'
|
||||
import { filterProjectsFromDir } from '@pnpm/workspace.projects-filter'
|
||||
@@ -103,25 +104,32 @@ export async function main (inputArgv: string[]): Promise<void> {
|
||||
workspaceDir,
|
||||
onlyInheritDlxSettingsFromLocal: isDlxOrCreateCommand,
|
||||
}) as { config: typeof config, context: ConfigContext })
|
||||
if (cmd !== 'setup' && context.wantedPackageManager != null && !shouldSkipPmHandling(cmd, cliParams)) {
|
||||
const pm = context.wantedPackageManager
|
||||
if (pm.onFail !== 'ignore') {
|
||||
if (pm.name === 'pnpm' && pm.onFail === 'download' && !isExecutedByCorepack()) {
|
||||
// Corepack owns version switching; pnpm only switches versions when
|
||||
// the user is running pnpm directly.
|
||||
await switchCliVersion(config, context)
|
||||
} else if (cliOptions.global) {
|
||||
globalWarn('Using --global skips the package manager check for this project')
|
||||
} else {
|
||||
// checkPackageManager and syncEnvLockfile run regardless of how pnpm
|
||||
// was invoked. Different developers on the same project may use
|
||||
// corepack or invoke pnpm directly, and the lockfile's
|
||||
// `packageManagerDependencies` entry must stay consistent across both
|
||||
// workflows. syncEnvLockfile self-gates via shouldPersistLockfile so
|
||||
// it only writes to the lockfile when the project opted in (via
|
||||
// `devEngines.packageManager`, or a v12+ `packageManager` pin).
|
||||
checkPackageManager(pm, { underCorepack: isExecutedByCorepack() })
|
||||
await syncEnvLockfile(config, context)
|
||||
if (cmd !== 'setup' && !shouldSkipPmHandling(cmd, cliParams)) {
|
||||
if (context.wantedPackageManager != null) {
|
||||
const pm = context.wantedPackageManager
|
||||
if (pm.onFail !== 'ignore') {
|
||||
if (pm.name === 'pnpm' && pm.onFail === 'download' && !isExecutedByCorepack()) {
|
||||
// Corepack owns version switching; pnpm only switches versions when
|
||||
// the user is running pnpm directly.
|
||||
await switchCliVersion(config, context)
|
||||
} else if (cliOptions.global) {
|
||||
globalWarn('Using --global skips the package manager check for this project')
|
||||
} else {
|
||||
// checkPackageManager and syncEnvLockfile run regardless of how pnpm
|
||||
// was invoked. Different developers on the same project may use
|
||||
// corepack or invoke pnpm directly, and the lockfile's
|
||||
// `packageManagerDependencies` entry must stay consistent across both
|
||||
// workflows. syncEnvLockfile self-gates via shouldPersistLockfile so
|
||||
// it only writes to the lockfile when the project opted in (via
|
||||
// `devEngines.packageManager`, or a v12+ `packageManager` pin).
|
||||
checkPackageManager(pm, { underCorepack: isExecutedByCorepack() })
|
||||
await syncEnvLockfile(config, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cmd != null && !cliOptions.global) {
|
||||
for (const runtime of getWantedRuntimes(context)) {
|
||||
checkRuntime(runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,8 +402,8 @@ 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
|
||||
* Whether to skip the packageManager/runtime handling block (both auto
|
||||
* download and warn/error checks). 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
|
||||
@@ -443,3 +451,67 @@ function checkPackageManager (pm: EngineDependency, opts: { underCorepack: boole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RUNTIME_DISPLAY_NAMES: Record<RuntimeName, string> = {
|
||||
node: 'Node.js',
|
||||
deno: 'Deno',
|
||||
bun: 'Bun',
|
||||
}
|
||||
|
||||
// devEngines.runtime takes precedence over engines.runtime per the iteration
|
||||
// order below: the first entry seen for a given runtime wins.
|
||||
function getWantedRuntimes (context: ConfigContext): EngineDependency[] {
|
||||
const manifest = context.rootProjectManifest
|
||||
if (manifest == null) return []
|
||||
const result: EngineDependency[] = []
|
||||
const seen = new Set<RuntimeName>()
|
||||
for (const enginesFieldName of ['devEngines', 'engines'] as const) {
|
||||
const enginesRuntime = manifest[enginesFieldName]?.runtime
|
||||
if (enginesRuntime == null) continue
|
||||
const runtimes: EngineDependency[] = Array.isArray(enginesRuntime) ? enginesRuntime : [enginesRuntime]
|
||||
for (const runtime of runtimes) {
|
||||
if (!runtime.name || !isRuntimeAlias(runtime.name) || seen.has(runtime.name)) continue
|
||||
seen.add(runtime.name)
|
||||
result.push(runtime)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function checkRuntime (runtime: EngineDependency): void {
|
||||
if (runtime.onFail == null || runtime.onFail === 'ignore' || runtime.onFail === 'download') return
|
||||
if (!runtime.name || !isRuntimeAlias(runtime.name)) return
|
||||
const runtimeName: RuntimeName = runtime.name
|
||||
const displayName = RUNTIME_DISPLAY_NAMES[runtimeName]
|
||||
const wantedRange = runtime.version
|
||||
if (!wantedRange || !semver.validRange(wantedRange)) {
|
||||
const msg = wantedRange
|
||||
? `This project requires an invalid ${displayName} version range: ${wantedRange}`
|
||||
: `This project requires a ${displayName} runtime but does not specify a version range`
|
||||
failRuntimeCheck(runtime.onFail, msg)
|
||||
return
|
||||
}
|
||||
const currentVersion = getSystemRuntimeVersion(runtimeName)
|
||||
if (currentVersion == null) {
|
||||
failRuntimeCheck(
|
||||
runtime.onFail,
|
||||
`This project requires ${displayName} ${wantedRange}, but ${displayName} was not found on the system`
|
||||
)
|
||||
return
|
||||
}
|
||||
if (semver.satisfies(currentVersion, wantedRange, { includePrerelease: true })) return
|
||||
|
||||
failRuntimeCheck(
|
||||
runtime.onFail,
|
||||
`This project requires ${displayName} ${wantedRange}. Your current ${displayName} is ${currentVersion}`
|
||||
)
|
||||
}
|
||||
|
||||
function failRuntimeCheck (onFail: 'error' | 'warn', message: string): void {
|
||||
if (onFail === 'error') {
|
||||
throw new PnpmError('BAD_RUNTIME_VERSION', message, { hint: RUNTIME_ON_FAIL_HINT })
|
||||
}
|
||||
globalWarn(message)
|
||||
}
|
||||
|
||||
const RUNTIME_ON_FAIL_HINT = 'If you want to bypass this version check, set "runtimeOnFail" to "warn" or "ignore" (e.g. via --runtime-on-fail=ignore), or set "devEngines.runtime.onFail"/"engines.runtime.onFail" to "warn" or "ignore"'
|
||||
|
||||
@@ -143,6 +143,257 @@ test('devEngines.packageManager with onFail=ignore should not check version', as
|
||||
expect(stderr.toString()).not.toContain('0.0.1')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=error should fail on Node.js version mismatch', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '99999.0.0',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires Node.js 99999.0.0')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=warn should warn on Node.js version mismatch', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '99999.0.0',
|
||||
onFail: 'warn',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
const output = stdout.toString() + stderr.toString()
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(output).toContain('This project requires Node.js 99999.0.0')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=ignore should not check Node.js version', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '99999.0.0',
|
||||
onFail: 'ignore',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
const output = stdout.toString() + stderr.toString()
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(output).not.toContain('99999.0.0')
|
||||
})
|
||||
|
||||
test('engines.runtime with onFail=error should fail on Node.js version mismatch', async () => {
|
||||
prepare({
|
||||
engines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '99999.0.0',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires Node.js 99999.0.0')
|
||||
})
|
||||
|
||||
test('engines.runtime with onFail=warn should warn on Node.js version mismatch', async () => {
|
||||
prepare({
|
||||
engines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '99999.0.0',
|
||||
onFail: 'warn',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
const output = stdout.toString() + stderr.toString()
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(output).toContain('This project requires Node.js 99999.0.0')
|
||||
})
|
||||
|
||||
test('engines.runtime with onFail=error should fail on invalid Node.js version range', async () => {
|
||||
prepare({
|
||||
engines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: 'invalid range',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires an invalid Node.js version range: invalid range')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=error should fail on invalid Node.js version range', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: 'invalid range',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires an invalid Node.js version range: invalid range')
|
||||
expect(stderr.toString()).toContain('--runtime-on-fail=ignore')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=warn should warn on invalid Node.js version range', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: 'invalid range',
|
||||
onFail: 'warn',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
const output = stdout.toString() + stderr.toString()
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(output).toContain('This project requires an invalid Node.js version range: invalid range')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=error should fail when Node.js version range is missing', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires a Node.js runtime but does not specify a version range')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=error should fail on invalid Deno version range', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'deno',
|
||||
version: 'invalid range',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires an invalid Deno version range: invalid range')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=error should fail on invalid Bun version range', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'bun',
|
||||
version: 'invalid range',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires an invalid Bun version range: invalid range')
|
||||
})
|
||||
|
||||
test('devEngines.runtime array entries are checked beyond the first one', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: [
|
||||
{
|
||||
name: 'node',
|
||||
version: '*',
|
||||
onFail: 'error',
|
||||
},
|
||||
{
|
||||
name: 'deno',
|
||||
version: 'invalid range',
|
||||
onFail: 'error',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
|
||||
expect(status).toBe(1)
|
||||
expect(stderr.toString()).toContain('This project requires an invalid Deno version range: invalid range')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=ignore should not check Deno version', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'deno',
|
||||
version: 'invalid range',
|
||||
onFail: 'ignore',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout, stderr } = execPnpmSync(['--config.verify-deps-before-run=false', 'exec', 'node', '--version'])
|
||||
const output = stdout.toString() + stderr.toString()
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(output).not.toContain('invalid Deno version range')
|
||||
})
|
||||
|
||||
test('devEngines.runtime with onFail=error should not block version output', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '99999.0.0',
|
||||
onFail: 'error',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { status, stdout, stderr } = execPnpmSync(['--version'])
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(stdout.toString()).toMatch(/\d+\.\d+\.\d+/)
|
||||
expect(stderr.toString()).not.toContain('This project requires Node.js 99999.0.0')
|
||||
})
|
||||
|
||||
test('devEngines.packageManager defaults to onFail=download (#11676)', async () => {
|
||||
prepare({
|
||||
devEngines: {
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
{
|
||||
"path": "../engine/runtime/commands"
|
||||
},
|
||||
{
|
||||
"path": "../engine/runtime/system-version"
|
||||
},
|
||||
{
|
||||
"path": "../exec/commands"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user