What are peerOptional dependencies? #193510
Replies: 7 comments
-
|
It does not mean "ignore the constraint if the peer is present." If some other dependency causes The difference from not declaring the dependency is the compatibility contract: So your guess is basically right: npm should not install or leave an incompatible peer at the resolution point just because the peer was marked optional. |
Beta Was this translation helpful? Give feedback.
-
|
The Contract — Precisely Stated Why Arborist Nested peer@2 — It's Correct Behavior This is the correct resolution, not a bug. Here's what Arborist is reasoning through: peer@1 is placed at root to satisfy has-peer's regular peer dep. The key insight: Arborist doesn't "fetch peer@2 to satisfy a peerOptional." It fetches peer@2 because a conflict was detected and a valid nesting resolution existed. If no valid nesting were possible, it would correctly ERESOLVE. When Does Arborist Correctly ERESOLVE? Historical Context: This Was a Real Bug Area Bottom Line |
Beta Was this translation helpful? Give feedback.
This comment was marked as low quality.
This comment was marked as low quality.
-
|
Hey @everett1992, great question — this one confuses a lot of people, and the npm docs don't do a great job explaining it, so let me try to break it down in a way that actually sticks. Think of dependencies in npm as different levels of "how much do I need this thing": A regular dependency means "I need this, install it." An optionalDependency means "try to install it, but if it fails (maybe it needs a native build and the platform doesn't support it), just move on, it's fine." A peerDependency means "I don't install this myself, but whoever uses me needs to have it, and it better be a compatible version." Now peerOptional is the interesting one it means The key thing people get wrong: the "optional" part only refers to whether the package needs to be present. It does NOT mean the version constraint is relaxed. If someone has it installed but it's the wrong version, npm treats that as a real conflict — the same as a regular peer dependency. A real-world example: imagine you write a logging library that can optionally format output nicely if the user has "chalk" installed. You don't need to force everyone to install chalk, but if they do, you'll need v5 because that's the API you coded against. That's a peerOptional — chalk isn't required, but if it is, please use v5. The package.json looks like this: Under the hood, npm's tree solver (Arborist) handles conflicts by nesting incompatible versions within node_modules subdirectories. If it can't find any arrangement that works, you get the dreaded ERESOLVE error. This happens with peerOptional too — optional doesn't mean "ignore conflicts." One gotcha worth knowing: in complex dependency trees (like projects using jest + ts-jest), npm sometimes behaves inconsistently with peerOptional — it might fetch a version on first install and then mark it as extraneous on the next one. If you ever see packages appearing and disappearing between installs, this is likely why. It's a known edge case. Hope that clears things up — it's one of those npm concepts that makes total sense once someone explains it plainly, but is nearly impossible to figure out just from the docs. |
Beta Was this translation helpful? Give feedback.
-
|
Here’s the practical way to think about them: What is a peerOptional dependency?It’s a dependency that:
Defined as: How npm actually treats it1. npm does NOT install it by default
2. If it IS present, it must satisfy the version
3. npm may still install it indirectly (important)
This is what you’re observing:
This is not because peerOptional is “auto-installed,” but because: Key difference vs other dependency typesType | Installed automatically? | Required? | Version enforced? -- | -- | -- | -- dependency | ✅ Yes | ✅ Yes | ✅ Yes optionalDependencies | ✅ Yes (but can fail) | ❌ No | ❌ Not strict peerDependencies | ❌ No | ✅ Yes (must exist) | ✅ Strict peerOptional | ❌ No | ❌ No | ✅ Only if presentAnswer to your core question
Declaring 👉 “If this dependency exists, it MUST be compatible.” Without it:
About your observed behavior (important)What you described:
This suggests: 👉 npm is making a conflict-resolution decision that isn’t stable across installs That’s likely:
What would be ideal behavior?As you noted, better options could be:
TL;DR
If you’re working on fixing this in npm/Arborist, your intuition about preferring deduplication over fetching a new version is spot on. |
Beta Was this translation helpful? Give feedback.
This comment was marked as low quality.
This comment was marked as low quality.
-
|
These answers didn't really help me. One aspect I'm trying to understand is when peer Optional should be satisfied. I think a strict package manager like would only satisfy the peerOptional dependency if the direct consumer declared a compatible edge, otherwise it would leave it missing. Npm's hoist optimization already exposes packages to undeclared deps, so it makes sense that hoisting can also satisfy peerOptionals. They are not like 'feature dependencies' where the package can sue their presence to assume the feature was requested - they could just as well be accidentally hoisted. I ended up implementing a minor fix in this PR, peerOptionals will prefer a node that already exists in the sub-tree instead of resolving a new edge. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
🏷️ Discussion Type
Question
Body
I'm strugging to understand how peerOptional dependencies should behave, and what might be a bug.
peerOptionalare edges declared inpeerDependenciesandpeerDepsMeta[name].optionalshared^1.0.0 is a peerOptional dependency of lib.
npm's package.json docs say this about peerDependenciesMeta
This is different from
optionalDependencies, which npm saysBut what does npm actually do with peerOptionals? What's the difference between not declaring a dependency at all?
My guess would be that npm will warn, error, or build a different node_modules to prevent the peerOptional dependency from being resolved to a package that doesn't satisfy the dependency.
There's a test that checks this, 'properly fail on conflicted peerOptionals'
That covers a case where it's impossible to satisfy both requirements because they must both go to
node_modules/shared.There's another case that covers detect conflicts in transitive peerOptional deps.
In this test the conflict can be avoided by nesting the non-peer.
But npm nests the required dependency by fetching the peerOptional edge
That feels like it doesn't match the docs, npm did automatically fetch peer@2 to satisfy a peerOptional, but only because it would have conflicted otherwise. I'm not sure how else npm could solve the conflict tho. I think ideally has-peer-optional would be placed so it can't resolve peer (it's optional anyway). But that's not possible due to has-peer.
Question is related to this issue, which I'm trying to fix.
In this case nm/jest-util@28 is placed by @types/jest@28, Then jest@29 has to nest nm/jest-a/nm/jest-util@29, nm/jest-b/nm/jest-util@29, ect (21 copies of jest-util@29). When ts-jest is placed it conflicts with nm/jest-util@28, pushing it into nm/@types/jest/nm/jest-util. **But instead of de duplicating and hoisting the 21 copies of jest-util@29 that satisfy the peer-dependency it fetches a version that only satisfies the peerOptional edge (jest-util@30).
In subsequent
npm install's the jest-util@30 edge is marked extraneous and removed, so the second install produces different results.The observed behavior technically matches the 'detect conflicts in transitive peerOptional deps' test, and it feels valid, but bad.
https://github.com/npm/cli/tree/latest/workspaces/arborist
Beta Was this translation helpful? Give feedback.
All reactions