Skip to content

fix: run @pnpm/exe setup script in-process to fix version switching without Node.js#10696

Merged
zkochan merged 1 commit intov10from
fix/10687
Feb 26, 2026
Merged

fix: run @pnpm/exe setup script in-process to fix version switching without Node.js#10696
zkochan merged 1 commit intov10from
fix/10687

Conversation

@zkochan
Copy link
Copy Markdown
Member

@zkochan zkochan commented Feb 26, 2026

When pnpm is installed as a standalone executable in environments without a system Node.js (e.g. Docker containers), the @pnpm/exe preinstall script (node setup.js) fails because node is not on PATH. This broke version switching via the packageManager field in package.json since v10.30.2, which changed getCurrentPackageName() to return @pnpm/exe instead of platform-specific package names like @pnpm/linux-x64.

Install with --ignore-scripts and run the setup script in-process using createRequire, which uses the Node.js runtime already bundled in the standalone binary.

Closes #10687

…thout Node.js

When pnpm is installed as a standalone executable in environments without
a system Node.js (e.g. Docker containers), the `@pnpm/exe` preinstall
script (`node setup.js`) fails because `node` is not on PATH. This broke
version switching via the `packageManager` field in package.json since
v10.30.2, which changed `getCurrentPackageName()` to return `@pnpm/exe`
instead of platform-specific package names like `@pnpm/linux-x64`.

Install with `--ignore-scripts` and link the platform-specific binary
in-process instead. The setup logic is inlined because setup.js can't
be loaded at runtime: `require()` fails on ESM (pnpm v11+) and
`import()` is intercepted by pkg's virtual filesystem in standalone
executables.

Closes #10687

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes version switching via the packageManager field in package.json when pnpm is installed as a standalone executable in environments without a system Node.js installation (e.g., Docker containers). The issue (#10687) was introduced in v10.30.2 when getCurrentPackageName() started returning @pnpm/exe for standalone executables, which caused the package's preinstall script (node setup.js) to fail because node wasn't available on PATH.

Changes:

  • Replace --allow-build=@pnpm/exe with --ignore-scripts to skip all install scripts
  • Add --config.strict-dep-builds=false to prevent errors when scripts are ignored
  • Implement linkExePlatformBinary() function to replicate the setup.js logic in-process without requiring external Node.js
  • Update changeset documenting the fix

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
tools/plugin-commands-self-updater/src/installPnpmToTools.ts Adds in-process platform binary linking logic and modifies pnpm add command to use --ignore-scripts instead of allowing specific build scripts
.changeset/fix-exe-version-switch-no-nodejs.md Changeset documenting the bug fix for version switching without Node.js

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
}
fs.linkSync(src, dest)
fs.chmodSync(dest, 0o755)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chmod operation runs on all platforms including Windows, but on Windows file permissions work differently (ACLs instead of Unix permissions) and fs.chmodSync has limited effect. Consider moving this line inside an OS check to skip it on Windows, similar to how the original setup.js doesn't include chmod at all. Alternatively, wrap it in a try-catch to handle potential errors on platforms where chmod might not work as expected.

Suggested change
fs.chmodSync(dest, 0o755)
if (platform !== 'win') {
fs.chmodSync(dest, 0o755)
}

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +91
const executable = platform === 'win' ? 'pnpm.exe' : 'pnpm'
const platformPkgDir = path.join(stageDir, 'node_modules', '@pnpm', `${platform}-${arch}`)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executable name is hardcoded, but the original setup.js (pnpm/artifacts/exe/setup.js lines 16-17) reads it from the platform package's package.json bin field. While the current hardcoded values match what's in the packages today, reading from package.json would be more maintainable and robust if executable names ever change in the future. Consider reading the executable name from the platform package's package.json like the original implementation does.

Suggested change
const executable = platform === 'win' ? 'pnpm.exe' : 'pnpm'
const platformPkgDir = path.join(stageDir, 'node_modules', '@pnpm', `${platform}-${arch}`)
const platformPkgDir = path.join(stageDir, 'node_modules', '@pnpm', `${platform}-${arch}`)
let executable = platform === 'win' ? 'pnpm.exe' : 'pnpm'
try {
const platformPkgJsonPath = path.join(platformPkgDir, 'package.json')
const platformPkgRaw = fs.readFileSync(platformPkgJsonPath, 'utf8')
const platformPkg = JSON.parse(platformPkgRaw) as { bin?: string | Record<string, string> }
const bin = platformPkg?.bin
if (typeof bin === 'string') {
executable = path.basename(bin)
} else if (bin && typeof bin === 'object') {
const binTargets = Object.values(bin)
if (binTargets.length > 0 && typeof binTargets[0] === 'string') {
executable = path.basename(binTargets[0] as string)
}
}
} catch {
// Fallback to the default hardcoded executable name if anything goes wrong.
}

Copilot uses AI. Check for mistakes.
if (platform === 'win') {
const exePkgJsonPath = path.join(exePkgDir, 'package.json')
const exePkg = JSON.parse(fs.readFileSync(exePkgJsonPath, 'utf8'))
fs.writeFileSync(path.join(exePkgDir, 'pnpm'), 'This file intentionally left blank')
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No validation that exePkg.bin exists before accessing exePkg.bin.pnpm on line 109. If the @pnpm/exe package.json is malformed or missing the bin field, this will throw "Cannot read property 'pnpm' of undefined" which is not very helpful for debugging. Consider adding validation like if (!exePkg.bin || !exePkg.bin.pnpm) with a clearer error message, or at least a comment explaining that this is expected to throw if the package structure is invalid.

Suggested change
fs.writeFileSync(path.join(exePkgDir, 'pnpm'), 'This file intentionally left blank')
fs.writeFileSync(path.join(exePkgDir, 'pnpm'), 'This file intentionally left blank')
if (exePkg.bin == null) {
exePkg.bin = {}
} else if (typeof exePkg.bin !== 'object') {
throw new Error(`Invalid "bin" field in ${exePkgJsonPath}: expected an object`)
}

Copilot uses AI. Check for mistakes.
const executable = platform === 'win' ? 'pnpm.exe' : 'pnpm'
const platformPkgDir = path.join(stageDir, 'node_modules', '@pnpm', `${platform}-${arch}`)
const src = path.join(platformPkgDir, executable)
if (!fs.existsSync(src)) return
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent return when platform binary doesn't exist could lead to incomplete installations. The original setup.js (pnpm/artifacts/exe/setup.js) would throw an error if the binary file doesn't exist (at line 19 when fs.linkSync tries to link a non-existent source file). The early return here makes the installation succeed even if the platform-specific binary is missing (e.g., if the platform package is corrupted). Consider throwing an error instead to make the installation fail explicitly, which would be consistent with the original behavior and make debugging easier.

Suggested change
if (!fs.existsSync(src)) return
if (!fs.existsSync(src)) {
throw new Error(`Platform-specific pnpm binary not found at expected path: ${src}`)
}

Copilot uses AI. Check for mistakes.
@zkochan zkochan merged commit 1f7425b into v10 Feb 26, 2026
21 of 27 checks passed
@zkochan zkochan deleted the fix/10687 branch February 26, 2026 09:42
zkochan added a commit that referenced this pull request Feb 26, 2026
…thout Node.js (#10696)

When pnpm is installed as a standalone executable in environments without
a system Node.js (e.g. Docker containers), the `@pnpm/exe` preinstall
script (`node setup.js`) fails because `node` is not on PATH. This broke
version switching via the `packageManager` field in package.json since
v10.30.2, which changed `getCurrentPackageName()` to return `@pnpm/exe`
instead of platform-specific package names like `@pnpm/linux-x64`.

Install with `--ignore-scripts` and link the platform-specific binary
in-process instead. The setup logic is inlined because setup.js can't
be loaded at runtime: `require()` fails on ESM (pnpm v11+) and
`import()` is intercepted by pkg's virtual filesystem in standalone
executables.

Closes #10687
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants