Come vengono risolti i peer

Una delle migliori caratteristiche di pnpm è che in un progetto, una versione specifica di un pacchetto avrà sempre un set di dipendenze. There is one exception from this rule, though - packages with peer dependencies.

Le dipendenze peer vengono risolte dalle dipendenze installate più in alto nel grafico delle dipendenze, poiché condividono la stessa versione del genitore. Ciò significa che se foo@1.0.0 ha due peer (bar@^1 e baz@^1) allora potrebbe avere diversi insiemi di dipendenze nello stesso progetto.

- foo-parent-1
  - bar@1.0.0
  - baz@1.0.0
  - foo@1.0.0
- foo-parent-2
  - bar@1.0.0
  - baz@1.1.0
  - foo@1.0.0

Nell'esempio sopra, foo@1.0.0 è installato per foo-parent-1 e foo-parent-2. Entrambi i pacchetti hanno anche bar e baz, ma dipendono da diverse versioni di baz. Di conseguenza, foo@1.0.0 ha due diversi insiemi di dipendenze: uno con baz@1.0.0 e l'altro con baz@1.1.0. Per supportare questi casi di utilizzo, pnpm deve collegare fisicamente foo@1.0.0 tante volte quanti diversi set di dipendenze sono presenti.

Normalmente, se un pacchetto non ha dipendenze peer, è difficilmente collegato a una node_modules accanto ai collegamenti simbolici delle sue dipendenze, in questo modo:

node_modules
└── .pnpm
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo
    │       ├── qux   -> ../../qux@1.0.0/node_modules/qux
    │       └── plugh -> ../../plugh@1.0.0/node_modules/plugh
    ├── qux@1.0.0
    ├── plugh@1.0.0

Tuttavia, se pippo ha dipendenze peer, potrebbero esserci più gruppi di dipendenze, quindi creiamo gruppi diversi per diverse risoluzioni di dipendenze peer:

node_modules
└── .pnpm
    ├── foo@1.0.0_bar@1.0.0+baz@1.0.0
    │   └── node_modules
    │       ├── foo
    │       ├── bar   -> ../../bar@1.0.0/node_modules/bar
    │       ├── baz   -> ../../baz@1.0.0/node_modules/baz
    │       ├── qux   -> ../../qux@1.0.0/node_modules/qux
    │       └── plugh -> ../../plugh@1.0.0/node_modules/plugh
    ├── foo@1.0.0_bar@1.0.0+baz@1.1.0
    │   └── node_modules
    │       ├── foo
    │       ├── bar   -> ../../bar@1.0.0/node_modules/bar
    │       ├── baz   -> ../../baz@1.1.0/node_modules/baz
    │       ├── qux   -> ../../qux@1.0.0/node_modules/qux
    │       └── plugh -> ../../plugh@1.0.0/node_modules/plugh
    ├── bar@1.0.0
    ├── baz@1.0.0
    ├── baz@1.1.0
    ├── qux@1.0.0
    ├── plugh@1.0.0

Creiamo collegamenti simbolici a foo che si trova all'interno di foo@1.0.0_bar@1.0.0+baz@1.0.0 o a quello in foo@1.0.0_bar@1.0.0+baz@1.1.0. Di conseguenza, il risolutore del modulo Node.js troverà i peer corretti.

Se un pacchetto non ha dipendenze peer ma ha dipendenze con peer risolti più in alto nel grafico, allora quel pacchetto transitivo può apparire nel con diversi gruppi di dipendenze. Ad esempio, c'è il pacchetto a@1.0.0 con una singola dipendenza b@1.0.0. b@1.0.0 ha una dipendenza peer c@^1. a@1.0.0 non risolverà mai i peer di b@1.0.0, quindi diventa dipendente anche dai peer di b@1.0.0.

Ecco come apparirà quella struttura in node_modules. In questo esempio, a@1.0.0 dovrà apparire due volte nei node_modules - risolto una volta con c@1.0.0 e di nuovo con c@1.1.0.

node_modules
└── .pnpm
    ├── a@1.0.0_c@1.0.0
    │   └── node_modules
    │       ├── a
    │       └── b -> ../../b@1.0.0_c@1.0.0/node_modules/b
    ├── a@1.0.0_c@1.1.0
    │   └── node_modules
    │       ├── a
    │       └── b -> ../../b@1.0.0_c@1.1.0/node_modules/b
    ├── b@1.0.0_c@1.0.0
    │   └── node_modules
    │       ├── b
    │       └── c -> ../../c@1.0.0/node_modules/c
    ├── b@1.0.0_c@1.1.0
    │   └── node_modules
    │       ├── b
    │       └── c -> ../../c@1.1.0/node_modules/c
    ├── c@1.0.0
    ├── c@1.1.0

Cyclic dependencies#

Added in: v12.0.0-rc.5 (pnpm v12 only)

Packages may depend on each other in a cycle: a depends on b, and b — directly, or through further packages — depends back on a. When a package inside such a cycle has peer dependencies, there is no single point "higher in the graph" to resolve them from, because entering the cycle at a and entering it at b lead to different parents.

pnpm cuts every cycle at a fixed place. The packages that form a cycle are ordered by their package IDs, and the edges that close it are cut at the same point no matter where the installation walks into it. The cut edge is still a real dependency — it is resolved against an occurrence of its target taken at the project level — but the walk never travels around the cycle, so a package inside a cycle is no longer resolved once per path that reaches it. Peer dependencies of packages inside a cycle still resolve to the nearest match along that order.

Because the cut no longer depends on the path the installation takes, the lockfile is a function of the dependency graph alone. Repeated installs, reordered packages globs in pnpm-workspace.yaml, and reordered entries in package.json all produce a byte-identical lockfile. Projects with cycle-heavy dependency graphs also end up with a smaller lockfile, since packages inside a cycle no longer get a separate peer variant per path that reaches them.

Existing lockfiles keep working: --frozen-lockfile installs consume them unchanged, and installs that skip resolution leave them untouched. The first install that re-resolves — after a dependency change, for instance — re-keys the peer variants of cyclic packages once, which shows up as a one-time lockfile diff.

In pnpm v11, where the cut depends on the order the graph is walked, the same dependencies can produce different lockfiles depending on the order projects and dependencies are listed in.