Hello Dartisans,
I've recently come across a tricky edge case when it comes to versioning dependencies, in particular dependencies that are explicitly exported by the depender. For example, in the Dart repo, there are three packages that export other packages: unittest which exports matcher, scheduled_test which exports unittest, and polymer which exports observe. When one package exports another, the exported API is generally considered part of the exporter's API. Users' version constraints reflect this: many packages use the matcher APIs from unittest, but none of them depend on matcher directly.
The Problem
Thursday morning,
unittest 0.11.1+1 and
matcher 0.11.1+1 were the latest versions.
unittest had the constraint
matcher: ">=0.10.0 <0.12.0". Thursday afternoon, I committed
a CL that added the
isNotEmpty property to
matcher and bumped its version to
0.11.2.
Suppose a user creates a new project that depends on unittest. To make sure other users get a version of unittest that has all the features they're using, they use the constraint unittest: ">=0.11.1 <0.12.0". They then proceed to use isNotEmpty.
Now we have a problem. According to the version constraints, using unittest 0.11.1+1 and matcher 0.11.1+1 is perfectly valid. However, in practice, doing so will break the package, since it uses a feature that's only available in matcher 0.11.2.
The Solution
We need to ensure that a lower bound on a dependency restricts the features it provides in practice. Concretely, if a user writes unittest: ">=0.11.2 <0.12.0" they should be confident that import "package:unittest/unittest.dart" will always provide a superset of the functionality of the lowest version they can test against when they write their code.
The way to do this is to narrow the constraint on exported dependencies substantially. In the example above, unittest 0.11.1+1 would have the constraint matcher: ">=0.11.1 <0.11.2". Then when matcher 0.11.2 was released, unittest 0.11.2 would be released as well with the constraint matcher: ">=0.11.2 <0.11.3".
The tight lower bound ensures that a user's constraint of unittest: ">=0.11.1 <0.12.0" translates into a lower bound of 0.11.1 on matcher as well. The tight upper bound ensures that when matcher 0.11.2 is released, users won't get it without also getting a new version of unittest, signaling that they should change their lower bound if they're using any new features.
This does put more work on the shoulders of the authors of the exporting packages, since they need to rev whenever the package they export adds a new feature. That said, it also makes a certain amount of sense: by exporting another package's API, they're tightly coupling themselves to that package, so it's reasonable for the version constraint to reflect that.
I've updated the Dart repo packages to follow the new convention, and soon we'll update the versioning guidelines to document it as well. I'll also send out an email to all authors of pub.dartlang.org packages that export other packages suggesting that they switch to the new convention.
Let me know if you have any questions!
- Natalie