fix(scripts): resolve pnpm when npx or npm run exports npm's own CLI (#34271)

`npx` and `npm run` set npm_execpath to npm's own npm-cli.js/npx-cli.js,
so resolvePnpmCliInvocation ran npm with pnpm's arguments. Skip npm's CLI
by file name and fall back to PNPM_HOME / pnpm on PATH. Route
build-workspace-packages and terminal layout parity through the shared
resolver instead of their own npm_execpath parsing.

Fixes #29438

Co-authored-by: Lance Woodson <65194841+woodsonl@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-26 22:18:56 -07:00
committed by GitHub
co-authored by Lance Woodson
parent bff2843fca
commit 4514634faa
4 changed files with 29 additions and 17 deletions
+4 -6
View File
@@ -11,6 +11,7 @@ import {
} from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, relative, resolve } from 'node:path'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
const root = resolve(import.meta.dirname, '../..')
const PACKAGES = [{ name: '@orca/process-host', dir: 'src/packages/process-host' }]
@@ -90,14 +91,11 @@ function writeStamp(packageDir, inputs) {
function runPackageBuild(name) {
// Why not @orca/process-host: this builds it, so its dist may not exist yet.
const pnpmEntry = process.env.npm_execpath
const [program, args] = /\.[cm]?js$/.test(pnpmEntry ?? '')
? [process.execPath, [pnpmEntry, '--filter', name, 'build']]
: ['pnpm', ['--filter', name, 'build']]
const result = spawnSync(program, args, {
const { command, prefixArgs, shell } = resolvePnpmCliInvocation()
const result = spawnSync(command, [...prefixArgs, '--filter', name, 'build'], {
cwd: root,
stdio: 'inherit',
shell: program === 'pnpm' && process.platform === 'win32'
shell
})
if (result.error) {
throw result.error
+7 -1
View File
@@ -4,6 +4,8 @@ import { statSync } from 'node:fs'
import { win32 } from 'node:path'
const JS_CLI_EXTENSION = /\.[cm]?js$/i
// Why: `npx` and `npm run` export npm's own CLI as npm_execpath, which would run npm, not pnpm.
const NPM_OWN_CLI_PATH = /(?:^|[/\\])np[mx]-cli\.js$/i
function isFile(path) {
try {
@@ -20,7 +22,11 @@ export function resolvePnpmCliInvocation({
pnpmHome = process.env.PNPM_HOME,
fileExists = isFile
} = {}) {
if (typeof npmExecPath === 'string' && npmExecPath.length > 0) {
if (
typeof npmExecPath === 'string' &&
npmExecPath.length > 0 &&
!NPM_OWN_CLI_PATH.test(npmExecPath)
) {
if (JS_CLI_EXTENSION.test(npmExecPath)) {
return { command: nodeExecPath, prefixArgs: [npmExecPath], shell: false }
}
@@ -37,6 +37,20 @@ describe('resolvePnpmCliInvocation', () => {
})
})
it.each([
['/usr/share/nodejs/npm/bin/npx-cli.js', 'linux', 'pnpm', false],
['/usr/local/lib/node_modules/npm/bin/npm-cli.js', 'darwin', 'pnpm', false],
['/Users/dev/.volta/tools/image/npm/10.8.0/bin/npm-cli.js', 'darwin', 'pnpm', false],
['C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js', 'win32', 'pnpm.cmd', true]
])(
'ignores the npm CLI %s that `npx` or `npm run` exports',
(npmExecPath, platform, command, shell) => {
expect(
resolvePnpmCliInvocation({ npmExecPath, nodeExecPath, platform, pnpmHome: '' })
).toEqual({ command, prefixArgs: [], shell })
}
)
it('does not wrap a Windows native pnpm.exe in node', () => {
expect(
resolvePnpmCliInvocation({
+4 -10
View File
@@ -13,11 +13,12 @@ import {
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, join, resolve } from 'node:path'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { runProcessSync } from '@orca/process-host'
import { describeProcessFailure } from './process-failure-message.mjs'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
const repoRoot = resolve(import.meta.dirname, '../..')
const BUILD_STAMP = join('out', '.terminal-layout-parity-build')
@@ -88,16 +89,9 @@ function git(args) {
return result.stdout.trim()
}
// npm_execpath is pnpm's JS entry (run under node), a native pnpm binary (corepack), or unset.
function pnpm(args, cwd, env = process.env) {
const entry = process.env.npm_execpath
if (entry && /\.[cm]?js$/i.test(entry)) {
run(process.execPath, [entry, ...args], cwd, env)
} else if (entry && /pnpm/i.test(basename(entry)) && existsSync(entry)) {
run(entry, args, cwd, env)
} else {
run('pnpm', args, cwd, env)
}
const { command, prefixArgs } = resolvePnpmCliInvocation()
run(command, [...prefixArgs, ...args], cwd, env)
}
/** A detached worktree per commit, installed and built once; a failed build throws before stamping. */