mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-29 19:20:04 -04:00
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>
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { expect, jest, test } from '@jest/globals'
|
|
let isSea = false
|
|
|
|
jest.unstable_mockModule('@pnpm/cli.meta', () => ({
|
|
detectIfCurrentPkgIsExecutable: jest.fn(() => isSea),
|
|
}))
|
|
|
|
jest.unstable_mockModule('execa', () => ({
|
|
sync: jest.fn(() => ({
|
|
stdout: 'v10.0.0',
|
|
})),
|
|
}))
|
|
|
|
const {
|
|
getSystemNodeVersionNonCached,
|
|
getSystemDenoVersionNonCached,
|
|
getSystemBunVersionNonCached,
|
|
getSystemRuntimeVersion,
|
|
engineName,
|
|
} = await import('../lib/index.js')
|
|
const execa = await import('execa')
|
|
|
|
test('getSystemNodeVersion() executed from an executable pnpm CLI', () => {
|
|
isSea = true
|
|
expect(getSystemNodeVersionNonCached()).toBe('v10.0.0')
|
|
expect(execa.sync).toHaveBeenCalledWith('node', ['--version'])
|
|
})
|
|
|
|
test('getSystemNodeVersion() from a non-executable pnpm CLI', () => {
|
|
isSea = false
|
|
expect(getSystemNodeVersionNonCached()).toBe(process.version)
|
|
})
|
|
|
|
test('getSystemNodeVersion() returns undefined if execa.sync throws an error', () => {
|
|
// Mock execa.sync to throw an error
|
|
jest.mocked(execa.sync).mockImplementationOnce(() => {
|
|
throw new Error('not found: node')
|
|
})
|
|
|
|
isSea = true
|
|
expect(getSystemNodeVersionNonCached()).toBeUndefined()
|
|
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
|
|
// Node version, the caller forwards it to `engineName(version)`
|
|
// and the result reflects that pinned Node — not whatever pnpm
|
|
// itself is running on. Format-stable across `v`-prefixed and
|
|
// bare versions.
|
|
const major22 = `${process.platform};${process.arch};node22`
|
|
expect(engineName('22.11.0')).toBe(major22)
|
|
expect(engineName('v22.11.0')).toBe(major22)
|
|
})
|
|
|
|
test('engineName() falls back to the host Node when no override is provided', () => {
|
|
// No-arg call mirrors the pre-runtime-pin behaviour: anchor to
|
|
// `getSystemNodeVersion()` (which itself prefers shell `node` over
|
|
// `process.version` only when running as a SEA bundle — covered
|
|
// by the tests above). Non-SEA test environment, so the system
|
|
// version equals `process.version`.
|
|
isSea = false
|
|
const major = process.version.replace(/^v/, '').split('.')[0]
|
|
expect(engineName()).toBe(`${process.platform};${process.arch};node${major}`)
|
|
})
|