Tom MacWright

tom@macwright.com

Optimistic and Pessimistic Versioning

GroupPessimisticvendoringnpm + strict dependenciesnpm + semverOptimistic

Modern software is defined by dependencies. The explosion of open source work and movement from batteries-included languages like PHP to minimal languages like JavaScript has meant that most applications, and indeed most libraries, 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.

Dependency culture has led to occasional panic, like left-pad, and thoughtful rants, like underscore’s versioning.

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:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards-compatible manner, and
  3. PATCH version when you make backwards-compatible bug fixes.

Along with this meaning, Semver gives you a set of symbols that lets you say, for instance:

  • 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.

Distrust

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

save-exact=true

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.

See also