Prereleases and Npm

I recently published the first prereleases of D3 4.0 alpha to npm. Npm’s treatment of prereleases were surprising, so here’s a quick guide so you don’t fall on your face like me.

I was introduced to prereleases in a blog post announcing the npm semantic version calculator, which wrote:

By default, prerelease versions are not included in a range. This is because prerelease versions are meant to be unstable and are expected to have breaking changes. But sometimes you want to include them anyway.

And if you read the Semantic Versioning spec:

A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. … Pre-release versions have a lower precedence than the associated normal version. A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0–0.3.7, 1.0.0-x.7.z.92.

Perfect, I thought. I just release “4.0.0-alpha.1” instead of “4.0.0”, and npm will automatically detect it as a prerelease.

Sure enough, it worked! The default install remained 3.5.12:

$ npm install --save d3
└── d3@3.5.12

And now you could explicitly opt-in to an alpha:

$ npm install --save d3@4.0.0-alpha.4
└─┬ d3@4.0.0-alpha.4
├── d3-array@0.7.0
├── d3-axis@0.1.0
├── d3-collection@0.1.0
├── d3-color@0.3.3
├── d3-dispatch@0.2.5
├── d3-dsv@0.1.13
├── d3-ease@0.5.1
├── d3-format@0.5.0
├── d3-interpolate@0.4.0
├── d3-path@0.1.3
├── d3-random@0.2.0
├── d3-request@0.3.0
├── d3-scale@0.5.0
├── d3-selection@0.5.1
├── d3-shape@0.4.0
├── d3-time@0.2.0
├── d3-time-format@0.3.0
└── d3-timer@0.1.0

(Note that you must specify an exact version, not simply d3@4, but this seems reasonable given that breaking changes are expected between alpha releases.)

Great, right? Except then I saw a concerning tweet:

Oops! So what went wrong?

Npm’s (Changing) Default Behavior

What happens when you install without specifying a version number?

npm install d3

In npm 3.3.12, which is the version of npm shipping with the latest stable release 5.4.1 of Node.js, this is equivalent to using the “*” semver range:

npm install d3@*

The special star range matches all versions, but per the above blog post, ranges do not include prereleases. The greatest matching version is then installed. Thus by default npm 3.3.12 installs the latest non-pre-release. You can confirm this using npm’s semver implementation:

var semver = require("semver");
semver.satisfies("4.0.0-alpha.1", "*"); // false

The star range was broken (matching prereleases), but this was fixed in May, 2015. Alas the fix has not yet been ported to the web-based calculator.

In npm 3.4.1, the default behavior changed: instead of the star range, npm now uses the “latest” tag, which is equivalent to:

npm install d3@latest

An npm distribution tag is conceptually similar to a git tag: it’s a name that points to an exact version. You can update a tag as often as you want, and the latest tag is automatically updated by npm whenever you publish.

Thus, since a couple months ago, npm by default installs the latest version including prereleases. (It is perhaps ironic that this change to semantic versioning was made in a patch release…) The release notes include an invitation for feedback, so I hope the npm team is reading:

We think this is what everyone wants, but if this causes problems for you, we want to know! If it proves problematic for people we will consider reverting it (preferrably before this becomes npm@latest).

I was burned, but I’m not sure the new behavior qualifies as problematic. On the one hand, installing a package without specifying version information is asking for trouble. On the other, a prerelease is by definition before a release, so perhaps it should be opt-in rather than installed by default.

I do hope the npm team updates the web-based semver calculator and its introductory blog post to clarify the default install behavior in regards to prereleases. The calculator should handle the star range correctly and support tags (at the very least, the latest tag).

Tagged Releases

To publish a prerelease without making it installed by default, you should specify a tag other than “latest”. For example, to set the “next” tag:

npm publish --tag next

Conveniently, this also allows the latest prerelease to be installed with the tag name instead of specifying an exact version:

npm install d3@next

Though, beware of breaking changes between alpha releases!

A big thanks to Aria Stewart for kindly explaining npm’s behavior and teaching me how to use distribution tags.