Repository navigation
Description
Last pnpm version that worked
11.28.8
pnpm version
12.15.0 (also 12.0.0-alpha.0 and 12.0.0)
Code to reproduce the issue
Two workspace projects use the same version of use-debounce, which has react as a peer. Project a uses React 18, project b uses React 19.
#!/bin/sh
set -e
PNPM_VERSION="${1:-12.15.0}"
rm -rf repro && mkdir -p repro/a repro/b && cd repro
echo '{ "name": "repro", "private": true }' > package.json
printf 'packages:\n - a\n - b\nnodeLinker: hoisted\nautoInstallPeers: false\n' > pnpm-workspace.yaml
echo '{ "name": "a", "version": "1.0.0", "dependencies": { "react": "18.3.1", "use-debounce": "10.0.4" } }' > a/package.json
echo '{ "name": "b", "version": "1.0.0", "dependencies": { "react": "19.1.0", "use-debounce": "10.0.4" } }' > b/package.json
npx -y "pnpm@${PNPM_VERSION}" install > /dev/null
echo "pnpm ${PNPM_VERSION}"
grep "^ use-debounce@10.0.4(" pnpm-lock.yaml
find . -path '*/node_modules/use-debounce/package.json' -not -path '*/.pnpm/*' | sort
for project in a b; do
node -e '
const path = require("node:path");
const project = path.resolve(process.argv[1]);
const debounceDir = path.dirname(require.resolve("use-debounce/package.json", { paths: [project] }));
const reactFrom = (dir) => require(require.resolve("react/package.json", { paths: [dir] })).version;
console.log(`${process.argv[1]}: react ${reactFrom(project)}, its use-debounce (${path.relative(".", debounceDir)}) sees react ${reactFrom(debounceDir)}`);
' "$project"
doneRun it as sh repro.sh 11.28.8 and sh repro.sh 12.15.0.
Expected behavior
The lockfile has two snapshots, use-debounce@10.0.4(react@18.3.1) and use-debounce@10.0.4(react@19.1.0). The hoisted layout should put one of them at the root and nest the other under b, so that each project's use-debounce sees that project's React. pnpm 11.28.8 does this:
pnpm 11.28.8
use-debounce@10.0.4(react@18.3.1):
use-debounce@10.0.4(react@19.1.0):
./b/node_modules/use-debounce/package.json
./node_modules/use-debounce/package.json
a: react 18.3.1, its use-debounce (node_modules/use-debounce) sees react 18.3.1
b: react 19.1.0, its use-debounce (b/node_modules/use-debounce) sees react 19.1.0
Actual behavior
pnpm 12 writes only the root copy. b resolves the root copy, and its use-debounce sees React 18:
pnpm 12.15.0
use-debounce@10.0.4(react@18.3.1):
use-debounce@10.0.4(react@19.1.0):
./node_modules/use-debounce/package.json
a: react 18.3.1, its use-debounce (node_modules/use-debounce) sees react 18.3.1
b: react 19.1.0, its use-debounce (node_modules/use-debounce) sees react 18.3.1
The lockfile is correct in both versions. Only the node_modules layout is wrong.
Additional information
- Real-world effect: a workspace uses
antd-img-crop@4.30.0with antd 5 in one project and with antd 6 in another. After a fresh install, the antd 6 project resolved the antd 5 variant. Its bundle then included a second copy of antd, and the crop modal crashed withTypeError: Cannot convert undefined or null to objectinflattenToken. The same kind of mismatch gave a React 19 project anextthat resolves React 18. - On 12.15.0,
--frozen-lockfile,--force,dedupeDirectDeps: false,dedupePeerDependents: false,dedupeInjectedDeps: false,hoistWorkspacePackages: falseandresolvePeersFromWorkspaceRoot: falsedo not change the result. - A repeat install on a
node_modulesthat pnpm 11 created keeps the nested copy. So the bug shows only on a fresh install, such as a new clone or a CI cache miss. - With the default
autoInstallPeers: true, pnpm 11 (11.5.3 and 11.28.8) also leaves out the nested copy. The repro setsautoInstallPeers: false, where 11 is correct and 12 is not.
Node.js version
v24.22.1 (macOS), v24.21.0 (Linux)
Operating System
macOS 27.0.1 (arm64) and Debian 12 bookworm (aarch64, node:24-slim Docker image)
Activity
github-actions commentedon Oct 26, 2026
@taneli-linear, are you planning to open a pull request for this? If so, reply here and a maintainer will assign the issue to you, so nobody else starts on the same fix.
taneli-linear commentedon Oct 26, 2026
No
[-]Regression in 12: with `nodeLinker: hoisted`, a second peer variant of a package is never nested, so a project resolves the wrong peer[/-][+]Regression in v12: with `nodeLinker: hoisted`, a second peer variant of a package is never nested, so a project resolves the wrong peer[/+]kairosci commentedon Oct 26, 2026
PR #17397 is open and covers the full issue. The fix preserves peer-context package identities when peers are not auto-installed and uses exact lockfile keys to link each project to its matching peer variant.
fix(hoist): preserve peer variants in workspaces (pnpm#17397)
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
js(deps) pnpm 12.15.0 -> 12.16.1
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Pin pnpm 12.16.0 and adapt the conversion tooling and settings to pnp…
Engineering - move to pnpm12 (#344791)
Update pnpm to v12.16.1 (#2164)