mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
fix(node): support pnpm 11+ global bin directory layout (#581)
* feat: add reproduction case for pnpm 11 engines mismatch - Add a new example project to reproduce issues when using pnpm 11 in engines configuration with '.x' notation. - Include a test definition to verify node and pnpm versions as detected by the environment. Generated-by: aiautocommit * test: add snapshot for node-pnpm-11-engines example Generated-by: aiautocommit * feat(node): support pnpm 11+ global bin directory layout * Resolve pnpm version via the context resolver to handle non-semver range notation (e.g., "11.5.x") found in `engines.pnpm`. * Update global bin directory path for pnpm 11+ to `PNPM_HOME/bin` to ensure `node-gyp` installation succeeds. * Add test case for resolved mise versions. Generated-by: aiautocommit
This commit is contained in:
+179
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"caches": {
|
||||
"apt": {
|
||||
"directory": "/var/cache/apt",
|
||||
"type": "locked"
|
||||
},
|
||||
"apt-lists": {
|
||||
"directory": "/var/lib/apt/lists",
|
||||
"type": "locked"
|
||||
},
|
||||
"node-modules": {
|
||||
"directory": "/app/node_modules/.cache",
|
||||
"type": "shared"
|
||||
},
|
||||
"pnpm-install": {
|
||||
"directory": "/opt/pnpm/store",
|
||||
"type": "shared"
|
||||
}
|
||||
},
|
||||
"deploy": {
|
||||
"base": {
|
||||
"step": "packages:apt:runtime"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"/mise/shims",
|
||||
"/mise/installs",
|
||||
"/usr/local/bin/mise",
|
||||
"/etc/mise/config.toml",
|
||||
"/root/.local/state/mise"
|
||||
],
|
||||
"step": "packages:mise"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"/app/node_modules"
|
||||
],
|
||||
"step": "build"
|
||||
},
|
||||
{
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".yarn"
|
||||
],
|
||||
"include": [
|
||||
"/root/.cache",
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "pnpm run start",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"assets": {
|
||||
"generated-mise-toml": "[generated-mise-toml]"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"path": "/mise/shims"
|
||||
},
|
||||
{
|
||||
"customName": "create mise config",
|
||||
"name": "generated-mise-toml",
|
||||
"path": "/etc/mise/config.toml"
|
||||
},
|
||||
{
|
||||
"cmd": "mise install",
|
||||
"customName": "install mise packages: node, pnpm"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.6.0"
|
||||
}
|
||||
],
|
||||
"name": "packages:mise",
|
||||
"variables": {
|
||||
"MISE_CACHE_DIR": "/mise/cache",
|
||||
"MISE_CONFIG_DIR": "/mise",
|
||||
"MISE_DATA_DIR": "/mise",
|
||||
"MISE_INSTALLS_DIR": "/mise/installs",
|
||||
"MISE_SHIMS_DIR": "/mise/shims"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"pnpm-install"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"path": "/app/node_modules/.bin"
|
||||
},
|
||||
{
|
||||
"cmd": "mkdir -p /app/node_modules/.cache"
|
||||
},
|
||||
{
|
||||
"dest": "package.json",
|
||||
"src": "package.json"
|
||||
},
|
||||
{
|
||||
"dest": "pnpm-lock.yaml",
|
||||
"src": "pnpm-lock.yaml"
|
||||
},
|
||||
{
|
||||
"path": "/opt/pnpm/bin"
|
||||
},
|
||||
{
|
||||
"cmd": "pnpm add -g node-gyp"
|
||||
},
|
||||
{
|
||||
"cmd": "pnpm install --frozen-lockfile --prefer-offline"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "packages:mise"
|
||||
}
|
||||
],
|
||||
"name": "install",
|
||||
"variables": {
|
||||
"CI": "true",
|
||||
"NODE_ENV": "production",
|
||||
"NPM_CONFIG_FUND": "false",
|
||||
"NPM_CONFIG_PRODUCTION": "false",
|
||||
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
|
||||
"PNPM_HOME": "/opt/pnpm",
|
||||
"PNPM_STORE_DIR": "/opt/pnpm/store"
|
||||
}
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"node-modules"
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"step": "install"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"caches": [
|
||||
"apt",
|
||||
"apt-lists"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libatomic1'",
|
||||
"customName": "install apt packages: libatomic1"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.6.0"
|
||||
}
|
||||
],
|
||||
"name": "packages:apt:runtime"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -285,6 +285,11 @@ func TestUsesPnpmBinSubdir(t *testing.T) {
|
||||
version: "workspace:^",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "resolved mise version",
|
||||
version: "11.5.1",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -146,12 +146,18 @@ func (p PackageManager) installDeps(ctx *generate.GenerateContext, install *gene
|
||||
if !usingCorepack {
|
||||
pnpmBinPath := PNPM_HOME
|
||||
|
||||
// newer versions of pnpm use a different bin directory
|
||||
if requestedPnpm := ctx.Resolver.Get("pnpm"); requestedPnpm != nil && usesPnpmBinSubdir(requestedPnpm.Version) {
|
||||
// pnpm 11+ installs global bins under PNPM_HOME/bin.
|
||||
//
|
||||
// We compare against the mise-resolved version rather than the requested one solely to handle
|
||||
// the Node ecosystem's "x-range" engines notation (e.g. `engines.pnpm: "11.5.x"`), which is not
|
||||
// valid semver and so cannot be parsed directly. Mise resolves it to a concrete version
|
||||
// (e.g. "11.5.1") that we can compare. For any other source (exact version, mise.toml, etc.)
|
||||
// resolving vs. using the requested string would yield the same result.
|
||||
if usesPnpmBinSubdir(resolvePnpmVersion(ctx)) {
|
||||
pnpmBinPath = PNPM_HOME + "/bin"
|
||||
}
|
||||
|
||||
// binaries are installed in the /bin subpath. If this is not added to PATH `pnpm add -g` will fail
|
||||
// The path must be added before `pnpm add -g`, otherwise pnpm errors that the global bin dir is not in PATH
|
||||
install.AddPaths([]string{pnpmBinPath})
|
||||
install.AddCommand(plan.NewExecCommand("pnpm add -g node-gyp"))
|
||||
}
|
||||
@@ -171,7 +177,7 @@ func (p PackageManager) installDeps(ctx *generate.GenerateContext, install *gene
|
||||
}
|
||||
}
|
||||
|
||||
// pnpm <= 11 used PNPM_HOME for bins, but pnpm 11+ uses a "bin" subdirectory within the PNPM_HOME directory
|
||||
// pnpm < 11 used PNPM_HOME for bins; pnpm 11+ uses a "bin" subdirectory within PNPM_HOME
|
||||
func usesPnpmBinSubdir(version string) bool {
|
||||
version = strings.TrimSpace(version)
|
||||
if version == "" {
|
||||
@@ -190,6 +196,29 @@ func usesPnpmBinSubdir(version string) bool {
|
||||
return pnpmVersion.Compare(pnpmPathLayoutVersion) >= 0
|
||||
}
|
||||
|
||||
// resolvePnpmVersion returns the mise-resolved pnpm version (e.g. "11.5.1").
|
||||
//
|
||||
// This exists only to normalize the Node ecosystem's x-range engines notation (e.g. "11.5.x") into a
|
||||
// concrete semver. We let mise do the resolution instead of reimplementing npm's range semantics ourselves.
|
||||
// Falls back to the requested version string if resolution fails; the real error is surfaced later when
|
||||
// the context resolves packages for the plan.
|
||||
//
|
||||
// Some build configuration depends on the exact pnpm version that exists, which is why this is critical.
|
||||
// TODO we should make this a generic function with a mise tool name param
|
||||
func resolvePnpmVersion(ctx *generate.GenerateContext) string {
|
||||
if pkgs, err := ctx.Resolver.ResolvePackages(); err == nil {
|
||||
if pnpm, ok := pkgs["pnpm"]; ok && pnpm.ResolvedVersion != nil {
|
||||
return *pnpm.ResolvedVersion
|
||||
}
|
||||
}
|
||||
|
||||
if requested := ctx.Resolver.Get("pnpm"); requested != nil {
|
||||
return requested.Version
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p PackageManager) PruneDeps(ctx *generate.GenerateContext, prune *generate.CommandStepBuilder) {
|
||||
prune.AddCache(p.GetInstallCache(ctx))
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function getPnpmVersion() {
|
||||
const ua = process.env.npm_config_user_agent || "";
|
||||
return /\bpnpm\/([^\s]+)/.exec(ua)?.[1] ?? null;
|
||||
}
|
||||
|
||||
const nodeVersion = process.version;
|
||||
console.log(`Node.js version: ${nodeVersion}`);
|
||||
|
||||
const pnpmVersionFromUA = getPnpmVersion();
|
||||
if (pnpmVersionFromUA) {
|
||||
console.log(`PNPM version (from user agent): v${pnpmVersionFromUA}`);
|
||||
} else {
|
||||
console.log('PNPM version (from user agent): not detected');
|
||||
}
|
||||
|
||||
try {
|
||||
const pnpmVersion = execSync('pnpm --version', { encoding: 'utf8' }).trim();
|
||||
console.log(`PNPM version (from CLI): v${pnpmVersion}`);
|
||||
} catch (error) {
|
||||
console.log('PNPM version (from CLI): not available');
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "node-pnpm-11-engines",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Reproduces railpack#577: pnpm 11 via engines (mise) fails on pnpm add -g node-gyp when engines uses .x notation",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "node main.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "24.16.0",
|
||||
"pnpm": "11.5.x"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.: {}
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"expectedOutput": [
|
||||
"Node.js version: v24.16.0",
|
||||
"PNPM version (from CLI): v11."
|
||||
],
|
||||
"stderrAllowed": true
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user