mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-29 11:11:43 -04:00
fix: global bin shim invokes pnpm instead of Node when installed via @pnpm/exe (#11335)
* fix(installation): skip pnpm exe when no Node.js is on PATH
When pnpm is installed as @pnpm/exe (a Single Executable Application
that bundles Node.js into the pnpm binary) and the user has no separate
Node.js on PATH, `which('node')` fails and `getNodeExecPath` used to
fall back to `process.execPath` - which in @pnpm/exe is the pnpm binary
itself, not a Node binary. That path got baked into generated global
bin shims via nodeExecPath, so running any globally-installed CLI
invoked pnpm with the target script as its first positional arg,
producing `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` from the current
working directory.
Detect the @pnpm/exe case via `detectIfCurrentPkgIsExecutable` and
return undefined from the fallback so the shim emits a plain
`exec node <target>` instead.
Closes #11291
Refs #4645
* Update pkg-manager/plugin-commands-installation/src/nodeExecPath.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
6
.changeset/fix-global-shim-pnpm-exe.md
Normal file
6
.changeset/fix-global-shim-pnpm-exe.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Globally-installed bins no longer fail with `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` when pnpm was installed via the standalone `@pnpm/exe` binary (e.g. `curl -fsSL https://get.pnpm.io/install.sh | sh -`) on a system without a separate Node.js installation. Previously, when `which('node')` failed during `pnpm add --global`, pnpm fell back to `process.execPath`, which in `@pnpm/exe` is the pnpm binary itself — and that path was baked into the generated bin shim, causing the shim to invoke pnpm instead of Node [#11291](https://github.com/pnpm/pnpm/issues/11291), [#4645](https://github.com/pnpm/pnpm/issues/4645).
|
||||
@@ -33,6 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/cli-meta": "workspace:*",
|
||||
"@pnpm/cli-utils": "workspace:*",
|
||||
"@pnpm/colorize-semver-diff": "catalog:",
|
||||
"@pnpm/command": "workspace:*",
|
||||
|
||||
@@ -268,7 +268,7 @@ when running add/update with the --workspace option')
|
||||
}
|
||||
if (opts.global && opts.pnpmHomeDir != null) {
|
||||
const nodeExecPath = await getNodeExecPath()
|
||||
if (isSubdir(opts.pnpmHomeDir, nodeExecPath)) {
|
||||
if (nodeExecPath != null && isSubdir(opts.pnpmHomeDir, nodeExecPath)) {
|
||||
installOpts['nodeExecPath'] = nodeExecPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import { detectIfCurrentPkgIsExecutable } from '@pnpm/cli-meta'
|
||||
import which from 'which'
|
||||
|
||||
export async function getNodeExecPath (): Promise<string> {
|
||||
export async function getNodeExecPath (): Promise<string | undefined> {
|
||||
try {
|
||||
// The system default Node.js executable is preferred
|
||||
// not the one used to run the pnpm CLI.
|
||||
@@ -9,6 +10,12 @@ export async function getNodeExecPath (): Promise<string> {
|
||||
return fs.realpath(nodeExecPath)
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (err['code'] !== 'ENOENT') throw err
|
||||
// When pnpm runs as @pnpm/exe (a packaged executable that bundles
|
||||
// Node.js into the pnpm binary), process.execPath points at the pnpm
|
||||
// binary itself rather than a standalone Node binary. Using it as the
|
||||
// nodeExecPath of a bin shim makes the shim invoke pnpm instead of
|
||||
// Node, which breaks globally-installed CLIs. See #11291 and #4645.
|
||||
if (detectIfCurrentPkgIsExecutable()) return undefined
|
||||
return process.env.NODE ?? process.execPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ const DEFAULT_OPTIONS = {
|
||||
|
||||
test('globally installed package is linked with active version of Node.js', async () => {
|
||||
const nodeExecPath = await getNodeExecPath()
|
||||
if (nodeExecPath == null) throw new Error('Node.js executable was not found')
|
||||
prepare()
|
||||
await add.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { detectIfCurrentPkgIsExecutable } from '@pnpm/cli-meta'
|
||||
import which from 'which'
|
||||
import { getNodeExecPath } from '../lib/nodeExecPath.js'
|
||||
|
||||
jest.mock('which', () => jest.fn())
|
||||
jest.mock('@pnpm/cli-meta', () => ({
|
||||
detectIfCurrentPkgIsExecutable: jest.fn(),
|
||||
}))
|
||||
|
||||
const whichMock = jest.mocked(which)
|
||||
const detectMock = jest.mocked(detectIfCurrentPkgIsExecutable)
|
||||
|
||||
afterEach(() => {
|
||||
whichMock.mockReset()
|
||||
detectMock.mockReset()
|
||||
})
|
||||
|
||||
test('returns undefined when node is not on PATH and pnpm is running as @pnpm/exe', async () => {
|
||||
const enoent: NodeJS.ErrnoException = Object.assign(new Error('not found: node'), { code: 'ENOENT' })
|
||||
whichMock.mockRejectedValue(enoent)
|
||||
detectMock.mockReturnValue(true)
|
||||
|
||||
await expect(getNodeExecPath()).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
test('falls back to process.execPath when node is not on PATH and pnpm is running under a real Node.js', async () => {
|
||||
const enoent: NodeJS.ErrnoException = Object.assign(new Error('not found: node'), { code: 'ENOENT' })
|
||||
whichMock.mockRejectedValue(enoent)
|
||||
detectMock.mockReturnValue(false)
|
||||
const savedNode = process.env.NODE
|
||||
delete process.env.NODE
|
||||
try {
|
||||
await expect(getNodeExecPath()).resolves.toBe(process.execPath)
|
||||
} finally {
|
||||
if (savedNode != null) process.env.NODE = savedNode
|
||||
}
|
||||
})
|
||||
@@ -24,6 +24,9 @@
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../cli/cli-meta"
|
||||
},
|
||||
{
|
||||
"path": "../../cli/cli-utils"
|
||||
},
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -5754,6 +5754,9 @@ importers:
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/types
|
||||
'@pnpm/cli-meta':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/cli-meta
|
||||
'@pnpm/cli-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/cli-utils
|
||||
|
||||
Reference in New Issue
Block a user