Modern software is defined by dependencies. The explosion of open source work
and movement from batteries-included languages like PHP to minimal languages
require some other dependency. If it’s a web application, it’ll use a framework,
included as a dependency. Or a database library, or so on.
Having maintained distributed libraries and applications in Node.js with npm,
as well as Python libraries and extensively tinkered with Go & vendoring, I think
versioning can be thought of in terms of optimistic and pessimistic or
trustful or guarded
Versions are social contracts about regressions and breaking changes
Semver standardized the meaning of a version number:
MAJOR version when you make incompatible API changes,
MINOR version when you add functionality in a backwards-compatible manner, and
PATCH version when you make backwards-compatible bug fixes.
Along with this meaning, Semver gives you a set of symbols that lets you say,
I’ll accept this version and also any new version with PATCH changes
I’ll accept only exactly this version
I’ll accept any version
The semver calculator is an interactive demo of these
features. The implicit assumption is:
I completely trust the person who writes this code
to do the right thing, to correctly version their
project, and not introduce regressions or backwards-incompatible changes
in minor or patch versions.
There are lots of ways for distrust to sneak into this system.
This maintainer will innocently break their code and release a new broken version.
A nefarious contributor will willfully break it in a bad way.
The maintainer will disappear from the internet.
The maintainer will remove the project from the internet.
How much do these possibilities bother you? Should you try to avoid them always,
or would you rather deal with them when they happen? Of course, they’re rare,
but when they do occur, it’s bad news bears.
I think some rationale is:
If you’re distributing a module with dependencies on other modules, being
lenient about dependencies means that the code you write today will have
to work with code other people somewhere write months or years in the future.
If you value “keeping dependencies up to date,” then managing versions manually
will be annoying and semver-specifiers will save you time and work.
If you’ve been working in technology for a long time, you may have lost
faith in people.
Reflection in systems
As the chart at the top shows, I think three points on the continuum are:
npm + loose semver: very optimistic. You believe that people will do the right
thing and won’t introduce bugs. If they do introduce bugs or disappear, then
things will get really bad. But if they do a good job, then you benefit
from their work automatically and don’t need to manually increment numbers ever.
npm + no semver specifiers / strict semver: moderate. Setting an explicit version
can guard against lots of regressions, and using npm shrinkwrap
can help even more. Unpublishing dependencies is still really bad news.
vendoring: very pessimistic. Nothing happens automatically, but also no regressions
can be introduced by outside parties, and a developer unpublishing their project
or disappearing from the internet is no big deal: worst case, you have to develop
that dependency yourself. Vendoring means that you include the source code
of your dependencies in your project itself: they aren’t pulled from a system
like npm or crates. This is the default for Go and can
be easily replicated in other systems.
Where I’m at
I’ve shifted from the npm default of saving
dependencies with the ^ semver specifier to adding
To my ~/.npmrc file, so that whenever I run npm install somedep --save, it’ll
pin the version of that new dependency. In some projects that are distributed
as small modules, I’ve also started to tinker with versioning small dependencies,
like in mapbox-sdk-js. For larger
projects at Mapbox, like Mapbox Studio and Turf, we’ve also shifted to writing mono-repos
rather than trying to make everything modules at the outset.