lerna: peerDependencies shouldn't be updated by `lerna publish`

Given the following setup and using independent versioning:

packages
| -- foo
| -- bar
// foo/package.json
{
  "version" "0.2.1"
}
// bar/package.json
{
  "peerDependencies": {
    "foo": ">=0.2.0"
  },
  "devDependencies": {
    "foo": "^0.2.1"
  }
}

If you make changes in foo, and run lerna publish (patch)…

  1. foo will updated to 0.2.2, as expected.
  2. bar’s dev dependency on foo will be updated to ^0.2.2, also as expected.
  3. But bar’s peer depdenncy on foo will be overwritten to ^0.2.2, unexpectedly!

I think the peerDependencies tracking of Lerna should be disabled, since it’s impossible for Lerna to know whether a change would have forced a peer dependency upgrade or not.

Peer dependencies should remain as loose as possible, with the absolute earliest accepted version that works, reducing warning noise and increasing interop. With the current logic of forcing a bump with each publish, Lerna forces dependents to upgrade even if there’s no need to.

Related to https://github.com/lerna/lerna/issues/955

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 4
  • Comments: 53 (5 by maintainers)

Commits related to this issue

Most upvoted comments

That makes sense, but my point is that Lerna can’t actually know whether a package wants to extend itself to include the new peer dep and retain backwards compat, or whether it wants to start fresh with only the latest version included.

I think trying to do anything magic here is going to end up being wrong more often than it’s worth, especially when you consider it across multiple packages.

An in-terminal UI prompt would be great though! It could even just ask you if you want to “extend” or “reset” the peer dep range, potentially without having to ask you to remember the exact ranges to set.

That said, I think we should hold off on that and first just get Lerna to stop touching peer deps.

That seems strictly better than the current behavior; personally I’d rather lerna error out when that’s the case - ie, force me to manually resolve the conflict before publishing.

@ljharb Ah yes, that clarification I agree with, thank you.

Updating deps isn’t a violation of semver; updating the bottom of the range for peer deps - because it’s part of the API of the module - is absolutely semver-major.

The “worth worrying about” bit was not specifically aimed at peerDependencies, but the penchant for lerna to publish new versions of a dependent when the dependency changes.

In fact, I’m fully on-board the “ignore peerDependencies in lerna publish” boat, at this point. The dev dependency will need to be bumped as before, because that’s currently how lerna recognizes a matching sibling that needs a local relative symlink, etc. Does simply abandoning any modification of peerDependencies work for enzyme?

With >= you just reduce the maintenance burden, not eliminate it. You still have to implement fixes and update the version range when things do break.

And when things break, they break hard. You can release a patch version that fixes compatibility but your older packages will be forever broken.

Oh and tools won’t warn about it so you have to tell your users not to use anything older than x.y.z. Also have fun explaining to your users that they’re using the wrong version when bug reports start coming in.

For the record, using >= is insanely dangerous and reckless and is absolutely an antipattern. It’s not pragmatism or convenience to set yourself up for guaranteed future breakage when the next semver-major is released.

Okay so you prefer convenience over correctness.

I have one big problem with >= and it’s that npm (and yarn?) defaults to installing the highest version in a range on new installs.

If you publish a package today that depends on react >= 16.0.0 then someone who installs it 5 years from now will get whatever react will be the latest version in 2023 and there is just no way you can guarantee today that that will still work. But your package says it should work and that’s a real problem because now even tools won’t warn you that it may break.

Now imagine all the wasted time and effort if tools stop warning about version mismatches and everyone has to figure out for themselves if the packages they depend on are actually compatible or not.

I think correctness is still the right answer although I agree it means doing a lot of unnecessary work.

/edit to say that this doesn’t really apply to peerdependencies except the part about there not being a warning when there PROBABLY should be

I can agree with that. Let’s make it happen then?

+1 to what @ljharb said.

It is very common actually for modules to need to major bump, but for packages that depend on them to be unaffected by whatever piece happened to have a breaking chnage. (This is true for lots of libraries and utilities. Even React is a good example where 14 -> 15 and 15 -> 16 might not contain breaking changes for many libraries.)

Breaking changes can be runtime-detected; very often you can intentionally write code that handles more than one major version at a time of the same dependency.

The issue here is indeed that lerna can never guess.

I’d argue that introducing a bug is a breaking change so it breaks my second assumption. Addings bugs should always be semver-major. 😄

@StevenLiekens it’s pragmatic, yup.

Arguing for always going with the purest, “correct” pegging of peer dependencies is naive I think because it doesn’t take into account maintenance burden. It all depends on the stability of the APIs you’re building for, and the rate at which new versions as released. If the APIs are stable and development of new versions is continuous, you’re going to want to use >=. If not, and APIs are unstable, ^ is better.

You can see this in the node community, where almost all of the top packages use >= pegging for the engines field. (browserify, express, grunt, gulp, lodash, request, chalk, forever, etc.)

Whereas in the React ecosystem things are less standardized. More popular packages seem to use ^ (react-select, react-motion, react-intl, react-apollo, etc.), but there are still popular packages that use >= as well (react-router, react-helmet, etc.)

While new node versions require almost no extra work for people to republish packages, new React versions cause huge frenzies while every single one of the packages using ^ has to be re-published (across all of its active major versions) for the new React. I think it’s a huge waste of time considering how stable React’s APIs are, but it’s their call.


Either way, you have to decide for yourself how stable your own monorepo’s APIs are.

This issue is solved for me since peerDependencies are no longer being auto-incremented and causing breakages. Feel free to open another for your concerns to be addressed.

@StevenLiekens why are you not declaring the peer dependency as >=1.0.0 instead of ^1.0.0? Sounds like that would solve all your problems. That’s actually how all peer dependencies should be written, until a newer version comes along that is actually backwards incompatible.

Seems like you’d need to prompt the user for a decision at some point. peerDependencies are fraught with hidden assumptions/expectations.

We don’t get the warnings while developing packages (because yarn/npm install ignores peerdependencies).

We do get warnings when installing the released packages into other projects.

After updating foo 1.0.0 -> 2.0.0 bar 1.0.0 -> 1.0.1

warning “bar@1.0.1” has unmet peer dependency “foo@^1.0.0”

But in reality bar is compatible with foo@2.0.0 and sometimes even requires it when we decide to break backwards compatibility.

foo 1.0.0 -> 2.0.0 bar 1.0.0 -> 2.0.0

warning “bar@2.0.0” has unmet peer dependency “foo@^1.0.0”

I hope that makes sense.

the old behavior was introducing breaking changes to packages (via their peerDependencies) with any publish at the patch, minor, or major level.

Fair point for updates at the patch or the minor level. It makes sense that non-breaking changes should work everywhere without additional changes. I agree that Lerna should not touch the version range if it already covers the new version.

The part that I have a real problem with is when the version range doesn’t already cover the new version.

When foo@1.0.0 is published then the ^0.2.1 version range doesn’t cover it and that now requires manual intervention to fix. This is worse than having automatically updated version ranges that are too strict.

So how about changing Lerna to not touch peerDependencies unless existing version ranges don’t cover the updates?

@StevenLiekens the old behavior was introducing breaking changes to packages (via their peerDependencies) with any publish at the patch, minor, or major level. So it was very wrong, forcing users to continually bump their versions to eliminate the mismatches.

As for how Lerna manages minor/major when things are workflow only and not code-level breakages, I’m not sure.

So anyway can the old behavior be made available again with a feature toggle? Our project depends on the old behavior.

You can argue that the old behavior is not smart, but it’s not completely wrong. And it was automatically managed by the tools which leaves almost no room for human error. I’d rather have version ranges that are not “as loose as possible” than version ranges that are wrong because someone forgot to update the ranges manually or updated them incorrectly.

@ljharb true, totally agree! As long as the default is to not do any magic that might contain breaking changes in it I’m good with whatever.

It’s something that could probably be configured so that lerna doesn’t have to infer it, though.

#1187 is the first step toward resolving this, stopping lerna publish from making inappropriate semver-major changes to any local peerDependencies.