Skip to content

dedupePeerDependents doesn't deduplicate when there's a cyclic relationship between packages #11834

Description

@astegmaier

Verify latest release

  • I verified that the issue exists in the latest pnpm release

pnpm version

11.2.2

Which area(s) of pnpm are affected? (leave empty if unsure)

No response

Link to the code that reproduces this issue or a replay of the bug

https://github.com/astegmaier/playground-pnpm-circular-peers-bug

Reproduction steps

pnpm-workspace.yaml

packages:
  - 'packages/*'
dedupePeerDependents: true

packages/a/package.json

{
  "name": "a",
  "dependencies": {
    "esbuild": "^0.27.0",
    "webpack": "^5.107.0"
  }
}

packages/b/package.json

{
  "name": "b",
  "dependencies": {
    "webpack": "^5.107.0"
  }
}

webpack's package.json

{
  "name": "webpack",
  "dependencies": { 
    "terser-webpack-plugin": "^5.3.10" // terser-webpack-plugin points back to webpack as a required peer
    // ...
  }
}

terser-webpack-plugin's package.json

{
  "name": "terser-webpack-plugin",
  "peerDependencies": { 
    "webpack": "^5.1.0" // webpack points back at terser-webpack-plugin as a regular dependency
  },
  "peerDependenciesMeta": {
    "esbuild":   { "optional": true }
    // ...
  }
}

Actual pnpm-lock.yaml (two webpack snapshots)

snapshots:
  webpack@5.107.0:
    dependencies:
      terser-webpack-plugin: 5.6.0(webpack@5.107.0)
      ...
    transitivePeerDependencies:
      - esbuild
      - ...
  webpack@5.107.0(esbuild@0.27.7):
    dependencies:
      terser-webpack-plugin: 5.6.0(esbuild@0.27.7)(webpack@5.107.0)
      ...
    transitivePeerDependencies:
      - esbuild
      - ...

Describe the Bug

When a package's regular dependency forms a peer cycle with it (the dep declares a required peer back on its parent) and that dependency also declares an optional peer that some consumers activate but others don't, pnpm creates two parent snapshots of the parent and dedupePeerDependents fails to merge them.

In this particular case, the failure to merge them can cause TypeError: The 'compilation' argument must be an instance of Compilation when running webpack in code that spans packages that resolve to the different peer contexts. This symptom is similar to the one reported in #9427 (although no clear reproduction was identified there).

Expected Behavior

Expected pnpm-lock.yaml (only one webpack snapshot)

snapshots:
  webpack@5.107.0(esbuild@0.27.7):
    dependencies:
      terser-webpack-plugin: 5.6.0(esbuild@0.27.7)(webpack@5.107.0)
      ...

This seems to be the behavior the dedupePeerDependents docs promise for projects that share a dep and differ only on an optional peer: pick the richer snapshot, point both projects at it.

In fact, the docs use webpack + esbuild as their specific paradigm case:

let's say we have a workspace with two projects and both of them have webpack in their dependencies. webpack has esbuild in its optional peer dependencies, and one of the projects has esbuild in its dependencies. In this case, pnpm will link two instances of webpack to the node_modules/.pnpm directory: one with esbuild and another one without it… you may now use the dedupePeerDependents setting to deduplicate webpack when it has no conflicting peer dependencies… both projects will use the same webpack instance, which is the one that has esbuild resolved.

The only thing the docs get wrong is that esbuild is not a peerDependency of webpack -- it's a transitive peer pulled in by terser-webpack-plugin (and possibly other dependencies too).

Which Node.js version are you using?

24.16.0

Which operating systems have you used?

  • macOS
  • Windows
  • Linux

If your OS is a Linux based, which one it is? (Include the version if relevant)

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions