mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-27 18:46:18 -04:00
fix(link-bins): apply bin ownership overrides in conflict resolution (#10975)
BIN_OWNER_OVERRIDES was only used in checkGlobalBinConflicts for global installs. This change applies the same ownership rules in compareCommandsInConflict so that conflict resolution is consistent between global conflict checking and actual bin linking. This ensures packages like npm get priority for bins like npx even in non-global installs. Closes #10850 * test(link-bins): add missing fixture for bin-owner-override test * refactor: extract BIN_OWNER_OVERRIDES to @pnpm/package-bins Move shared logic to avoid code duplication between link-bins and checkGlobalBinConflicts. * fix(link-bins): use regex for Windows path compatibility in test * refactor(link-bins): remove redundant ownName field pkgOwnsBin already handles the binName === pkgName case, making the ownName field and its associated checks redundant. * Change versioning to patch for bins resolver and linker Added BIN_OWNER_OVERRIDES and pkgOwnsBin to @pnpm/bins.resolver for improved conflict resolution in bin linking. * test: remove node_modules from bin-owner-override fixture Move fixture packages to the directory root instead of nesting them inside node_modules, avoiding committing node_modules to the repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Zoltan Kochan <z@kochan.io> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
9
.changeset/apply-bin-owner-overrides-link-bins.md
Normal file
9
.changeset/apply-bin-owner-overrides-link-bins.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@pnpm/bins.resolver": patch
|
||||
"@pnpm/bins.linker": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Added `BIN_OWNER_OVERRIDES` and `pkgOwnsBin` to `@pnpm/bins.resolver`. Applied in bins.linker conflict resolution for consistent behavior between global conflict checking and actual bin linking, so packages like `npm` get priority for bins like `npx` even in non-global installs [#10850](https://github.com/pnpm/pnpm/issues/10850).
|
||||
|
||||
Removed the redundant `ownName` field from `CommandInfo` since `pkgOwnsBin` already handles the `binName === pkgName` case.
|
||||
@@ -2,7 +2,7 @@ import { existsSync, promises as fs } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
|
||||
import { type Command, getBinsFromPackageManifest } from '@pnpm/bins.resolver'
|
||||
import { type Command, getBinsFromPackageManifest, pkgOwnsBin } from '@pnpm/bins.resolver'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { readModulesDir } from '@pnpm/fs.read-modules-dir'
|
||||
import { globalWarn, logger } from '@pnpm/logger'
|
||||
@@ -122,7 +122,6 @@ export async function linkBinsOfPackages (
|
||||
}
|
||||
|
||||
interface CommandInfo extends Command {
|
||||
ownName: boolean
|
||||
pkgName: string
|
||||
pkgVersion: string
|
||||
makePowerShellShim: boolean
|
||||
@@ -169,8 +168,11 @@ function resolveCommandConflicts (group: CommandInfo[], binsDir: string): Comman
|
||||
}
|
||||
|
||||
function compareCommandsInConflict (a: CommandInfo, b: CommandInfo): number {
|
||||
if (a.ownName && !b.ownName) return 1
|
||||
if (!a.ownName && b.ownName) return -1
|
||||
// Check ownership: a package that owns the bin name gets priority
|
||||
const aOwns = pkgOwnsBin(a.name, a.pkgName)
|
||||
const bOwns = pkgOwnsBin(b.name, b.pkgName)
|
||||
if (aOwns && !bOwns) return 1
|
||||
if (!aOwns && bOwns) return -1
|
||||
if (a.pkgName !== b.pkgName) return a.pkgName.localeCompare(b.pkgName) // it's pointless to compare versions of 2 different package
|
||||
return semver.compare(a.pkgVersion, b.pkgVersion)
|
||||
}
|
||||
@@ -235,7 +237,6 @@ async function getPackageBinsFromManifest (manifest: DependencyManifest, pkgDir:
|
||||
}
|
||||
return cmds.map((cmd) => ({
|
||||
...cmd,
|
||||
ownName: cmd.name === manifest.name,
|
||||
pkgName: manifest.name,
|
||||
pkgVersion: manifest.version,
|
||||
makePowerShellShim: POWER_SHELL_IS_SUPPORTED && manifest.name !== 'pnpm',
|
||||
|
||||
1
bins/linker/test/fixtures/bin-owner-override/node/index.js
vendored
Normal file
1
bins/linker/test/fixtures/bin-owner-override/node/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log('node')
|
||||
1
bins/linker/test/fixtures/bin-owner-override/node/npx.js
vendored
Normal file
1
bins/linker/test/fixtures/bin-owner-override/node/npx.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log('npx from node')
|
||||
8
bins/linker/test/fixtures/bin-owner-override/node/package.json
vendored
Normal file
8
bins/linker/test/fixtures/bin-owner-override/node/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "node",
|
||||
"version": "1.0.0",
|
||||
"bin": {
|
||||
"node": "index.js",
|
||||
"npx": "npx.js"
|
||||
}
|
||||
}
|
||||
2
bins/linker/test/fixtures/bin-owner-override/npm/bin/npm-cli.js
vendored
Normal file
2
bins/linker/test/fixtures/bin-owner-override/npm/bin/npm-cli.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('npm cli')
|
||||
2
bins/linker/test/fixtures/bin-owner-override/npm/bin/npx-cli.js
vendored
Normal file
2
bins/linker/test/fixtures/bin-owner-override/npm/bin/npx-cli.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('npx cli')
|
||||
8
bins/linker/test/fixtures/bin-owner-override/npm/package.json
vendored
Normal file
8
bins/linker/test/fixtures/bin-owner-override/npm/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "npm",
|
||||
"version": "10.0.0",
|
||||
"bin": {
|
||||
"npm": "bin/npm-cli.js",
|
||||
"npx": "bin/npx-cli.js"
|
||||
}
|
||||
}
|
||||
2
bins/linker/test/fixtures/bin-owner-override/other-pkg/index.js
vendored
Normal file
2
bins/linker/test/fixtures/bin-owner-override/other-pkg/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('other-pkg npx')
|
||||
7
bins/linker/test/fixtures/bin-owner-override/other-pkg/package.json
vendored
Normal file
7
bins/linker/test/fixtures/bin-owner-override/other-pkg/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "other-pkg",
|
||||
"version": "1.0.0",
|
||||
"bin": {
|
||||
"npx": "index.js"
|
||||
}
|
||||
}
|
||||
@@ -697,3 +697,30 @@ describe('node binary linking', () => {
|
||||
expect(fs.existsSync(path.join(binTarget, `node${CMD_EXTENSION}`))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test('linkBins() resolves conflicts using BIN_OWNER_OVERRIDES (npx owned by npm)', async () => {
|
||||
const binTarget = temporaryDirectory()
|
||||
const binOwnerOverrideFixture = f.prepare('bin-owner-override')
|
||||
const warn = jest.fn()
|
||||
|
||||
await linkBins(binOwnerOverrideFixture, binTarget, { warn })
|
||||
|
||||
// npx should be linked from npm package (owner override), not node or other-pkg
|
||||
// BIN_OWNER_OVERRIDES says: npx is owned by npm
|
||||
expect(binsConflictLogger.debug).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
binaryName: 'npx',
|
||||
binsDir: binTarget,
|
||||
linkedPkgName: 'npm',
|
||||
skippedPkgName: expect.any(String),
|
||||
skippedPkgVersion: expect.any(String),
|
||||
})
|
||||
)
|
||||
|
||||
const binLocation = path.join(binTarget, 'npx')
|
||||
expect(fs.existsSync(binLocation)).toBe(true)
|
||||
const content = fs.readFileSync(binLocation, 'utf8')
|
||||
// npx should come from npm package, not node or other-pkg
|
||||
// Use a regex that matches both forward and backslashes for Windows compatibility
|
||||
expect(content).toMatch(/npm[/\\]bin[/\\]npx-cli\.js/)
|
||||
})
|
||||
|
||||
@@ -9,6 +9,20 @@ export interface Command {
|
||||
path: string
|
||||
}
|
||||
|
||||
// Maps a bin name to all packages that are legitimate owners of it, beyond
|
||||
// the default rule that a package named `X` owns the `X` bin. For example,
|
||||
// `npx` ships inside the `npm` package, and `pnpx` ships inside both the
|
||||
// `pnpm` package and the `@pnpm/exe` package.
|
||||
export const BIN_OWNER_OVERRIDES: Record<string, string[]> = {
|
||||
npx: ['npm'],
|
||||
pnpm: ['@pnpm/exe'],
|
||||
pnpx: ['pnpm', '@pnpm/exe'],
|
||||
}
|
||||
|
||||
export function pkgOwnsBin (binName: string, pkgName: string): boolean {
|
||||
return binName === pkgName || BIN_OWNER_OVERRIDES[binName]?.includes(pkgName) === true
|
||||
}
|
||||
|
||||
export async function getBinsFromPackageManifest (manifest: DependencyManifest, pkgPath: string): Promise<Command[]> {
|
||||
if (manifest.bin) {
|
||||
return commandsFromBin(manifest.bin, manifest.name, pkgPath)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { getBinsFromPackageManifest } from '@pnpm/bins.resolver'
|
||||
import { getBinsFromPackageManifest, pkgOwnsBin } from '@pnpm/bins.resolver'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import {
|
||||
type GlobalPackageInfo,
|
||||
@@ -10,20 +10,6 @@ import {
|
||||
import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader'
|
||||
import type { DependencyManifest } from '@pnpm/types'
|
||||
|
||||
// Maps a bin name to all packages that are legitimate owners of it, beyond
|
||||
// the default rule that a package named `X` owns the `X` bin. For example,
|
||||
// `npx` ships inside the `npm` package, and `pnpx` ships inside both the
|
||||
// `pnpm` package and the `@pnpm/exe` package.
|
||||
const BIN_OWNER_OVERRIDES: Record<string, string[]> = {
|
||||
npx: ['npm'],
|
||||
pnpm: ['@pnpm/exe'],
|
||||
pnpx: ['pnpm', '@pnpm/exe'],
|
||||
}
|
||||
|
||||
function pkgOwnsBin (binName: string, pkgName: string): boolean {
|
||||
return binName === pkgName || BIN_OWNER_OVERRIDES[binName]?.includes(pkgName) === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for bin name conflicts between new packages and existing global
|
||||
* packages. Returns a set of bin names that should be skipped during linking
|
||||
|
||||
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/.gitignore
vendored
Normal file
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
!**/node_modules/**/*
|
||||
!/node_modules/
|
||||
1
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/node/index.js
generated
vendored
Normal file
1
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/node/index.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log('node')
|
||||
1
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/node/npx.js
generated
vendored
Normal file
1
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/node/npx.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log('npx from node')
|
||||
8
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/node/package.json
generated
vendored
Normal file
8
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/node/package.json
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "node",
|
||||
"version": "1.0.0",
|
||||
"bin": {
|
||||
"node": "index.js",
|
||||
"npx": "npx.js"
|
||||
}
|
||||
}
|
||||
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/npm/bin/npm-cli.js
generated
vendored
Normal file
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/npm/bin/npm-cli.js
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('npm cli')
|
||||
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/npm/bin/npx-cli.js
generated
vendored
Normal file
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/npm/bin/npx-cli.js
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('npx cli')
|
||||
8
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/npm/package.json
generated
vendored
Normal file
8
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/npm/package.json
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "npm",
|
||||
"version": "10.0.0",
|
||||
"bin": {
|
||||
"npm": "bin/npm-cli.js",
|
||||
"npx": "bin/npx-cli.js"
|
||||
}
|
||||
}
|
||||
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/other-pkg/index.js
generated
vendored
Normal file
2
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/other-pkg/index.js
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('other-pkg npx')
|
||||
7
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/other-pkg/package.json
generated
vendored
Normal file
7
pkg-manager/link-bins/test/fixtures/bin-owner-override/node_modules/other-pkg/package.json
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "other-pkg",
|
||||
"version": "1.0.0",
|
||||
"bin": {
|
||||
"npx": "index.js"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user