Files
pnpm/pnpm11/cli/meta/test/getCurrentPackageName.test.ts
Zoltan KochanandClaude Opus 5.5 53428ea2dd fix(self-update): refuse to update a pnpm installed by Homebrew (#16564)
`pnpm self-update` installs the target pnpm into the global packages
directory and links it into PNPM_HOME. It never touches the Homebrew
keg, so a Homebrew user ended up with a second pnpm that the Homebrew
binary on PATH kept shadowing. The update reported success while
`pnpm -v` stayed the same, and later runs declined because the target
was "already installed globally".

Both CLIs now detect a running pnpm that lives inside a Homebrew keg
(`<cellar>/pnpm/<version>/` or `<cellar>/pnpm@<major>/<version>/`) from
its canonical path and fail with ERR_PNPM_CANT_SELF_UPDATE_IN_HOMEBREW,
pointing at `brew upgrade <formula>`. The check sits next to the
existing Corepack refusal, before project config is loaded.

Closes https://github.com/pnpm/pnpm/issues/16547

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:49:08 +02:00

64 lines
2.4 KiB
TypeScript

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, test } from '@jest/globals'
import { detectIfCurrentPkgIsExecutable, getCurrentPackageName, homebrewFormulaOf, isExecutedByCorepack } from '@pnpm/cli.meta'
describe('detectIfCurrentPkgIsExecutable()', () => {
test('returns false when not running as a SEA binary', () => {
// In a test environment node:sea is unavailable, so the require() inside
// detectIfCurrentPkgIsExecutable() throws and the catch block returns false.
expect(detectIfCurrentPkgIsExecutable()).toBe(false)
})
})
describe('getCurrentPackageName()', () => {
test('returns "pnpm" when not running as a SEA binary', () => {
expect(getCurrentPackageName()).toBe('pnpm')
})
})
describe('isExecutedByCorepack()', () => {
test('returns true when COREPACK_ROOT is set', () => {
expect(isExecutedByCorepack({ COREPACK_ROOT: '/usr/local/lib/corepack' })).toBe(true)
})
test('returns false when COREPACK_ROOT is not set', () => {
expect(isExecutedByCorepack({})).toBe(false)
})
test('returns false when COREPACK_ROOT is undefined', () => {
expect(isExecutedByCorepack({ COREPACK_ROOT: undefined })).toBe(false)
})
})
describe('homebrewFormulaOf()', () => {
let root: string
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-cli-meta-'))
})
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true })
})
function createKeg (formula: string, { receipt }: { receipt: boolean }): string {
const keg = path.join(root, 'Cellar', formula, '1.0.0')
const entry = path.join(keg, 'lib', 'node_modules', 'pnpm', 'dist', 'pnpm.mjs')
fs.mkdirSync(path.dirname(entry), { recursive: true })
if (receipt) fs.writeFileSync(path.join(keg, 'INSTALL_RECEIPT.json'), '{}')
return entry
}
test('returns the formula whose keg holds the path', () => {
expect(homebrewFormulaOf(createKeg('pnpm', { receipt: true }))).toBe('pnpm')
expect(homebrewFormulaOf(createKeg('pnpm@11', { receipt: true }))).toBe('pnpm@11')
})
test('returns undefined outside a pnpm keg', () => {
expect(homebrewFormulaOf(createKeg('node', { receipt: true }))).toBeUndefined()
expect(homebrewFormulaOf(createKeg('pnpm@10', { receipt: false }))).toBeUndefined()
expect(homebrewFormulaOf(path.join(root, 'pnpm.mjs'))).toBeUndefined()
})
})