Visitar URL original
Regression in v12: with `nodeLinker: hoisted`, a second peer variant of a package is never nested, so a project resolves the wrong peer · Issue #17395 · pnpm/pnpm · GitHub
Skip to content

Regression in v12: with nodeLinker: hoisted, a second peer variant of a package is never nested, so a project resolves the wrong peer #17395

Closed

Description

@taneli-linear

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"
done

Run 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.0 with 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 with TypeError: Cannot convert undefined or null to object in flattenToken. The same kind of mismatch gave a React 19 project a next that resolves React 18.
  • On 12.15.0, --frozen-lockfile, --force, dedupeDirectDeps: false, dedupePeerDependents: false, dedupeInjectedDeps: false, hoistWorkspacePackages: false and resolvePeersFromWorkspaceRoot: false do not change the result.
  • A repeat install on a node_modules that 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 sets autoInstallPeers: 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

  1. github-actions commented on Oct 26, 2026

    @github-actions
    Contributor

    @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.

  2. taneli-linear commented on Oct 26, 2026

    @taneli-linear
    Author

    2. @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.

    No

  3. changed the title [-]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[/+] on Oct 26, 2026
  4. kairosci commented on Oct 26, 2026

    @kairosci
    Contributor

    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.

  5. added a commit that references this issue on Oct 26, 2026
    76fa595
  6. added a commit that references this issue on Oct 31, 2026
  7. added a commit that references this issue on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions