If you sub­scribe to the phi­los­o­phy of Semantic Versioning (you should!) and use npm, there is a cer­tain be­hav­iour that might trip you up. If you don’t want to waste a full hour like I did, this might be help­ful.


I use Eleventy, and it has­n’t hit v1.0 yet. It is, there­fore, im­plic­itly sig­nalled by the de­vel­oper to not be ready for pro­duc­tion.

The pre­cur­sor to the v1.0 re­lease - v0.11.0 - was pub­lished re­cently.

As I was on v0.10.0, the lat­est up­date should have been sim­ple to up­grade to, right? Just us­ing the up­date op­tion on npm and spec­i­fy­ing the pack­age should have done, like so:

npm upgrade @11ty/eleventy

Well, turns out, this did­n’t work! 😭

Time to ex­plore.

How is a de­pen­dency spec­i­fied in pack­age.json by de­fault?

First, what hap­pens when you npm install some­thing?

npm install @11ty/eleventy

By de­fault, it saves the pack­age as a de­pen­dency in your pro­jec­t’s package.json file us­ing the caret syn­tax for the ver­sion scheme:

{
"dependencies": {
"@11ty/eleventy": "^0.10.0"
}
}

The caret syn­tax al­most al­ways means: if there is a [minor] ver­sion up­date or a [patch] up­date, but not a [major] up­date, down­load and save the new pack­age ver­sion.

Ok, so what?

Well, let’s de­mys­tify the al­most al­ways above, and you’ll see some in­ter­est­ing phe­nom­e­non.

For a pre v1.0 re­lease, also called a pre-re­lease, the rule I men­tioned above stands void — and that’s why the npm upgrade com­mand, by de­sign, is not meant to work in my case.

It fol­lows a dif­fer­ent ver­sion match­ing al­go­rithm. What is that? Look at npm’s doc­u­men­ta­tion on semver:

Allows changes that do not mod­ify the left-most non-zero digit in the [major, mi­nor, patch] tu­ple. In other words, this al­lows patch and mi­nor up­dates for ver­sions 1.0.0 and above, patch up­dates for ver­sions 0.X >=0.1.0, and no up­dates for ver­sions 0.0.X.

Many au­thors treat a 0.x ver­sion as if the x were the ma­jor breaking-change” in­di­ca­tor.

Caret ranges are ideal when an au­thor may make break­ing changes be­tween 0.2.4 and 0.3.0 re­leases, which is a com­mon prac­tice. However, it pre­sumes that there will not be break­ing changes be­tween 0.2.4 and 0.2.5. It al­lows for changes that are pre­sumed to be ad­di­tive (but non-break­ing), ac­cord­ing to com­monly ob­served prac­tices.

This means if Eleventy had a v0.10.1 re­lease, I would have had no trou­ble up­grad­ing. There is no break­ing change ex­pected in this re­lease.

If Eleventy had ver­sioned all its re­leases so far us­ing just the patch field, then no up­grade would have hap­pened be­tween, for ex­am­ple, v0.0.10 and v0.0.11. Since only the patch field is be­ing used, every re­lease is pre­sumed to be a break­ing change.

A sim­i­lar be­hav­iour ap­plies for pre-re­lease up­dates to pro­duc­tion (v1.0+) pack­ages. If a de­vel­oper has re­leased v1.1-alpha.1, it will be ig­nored by npm upgrade. An al­pha re­lease, of course, is not ready for pro­duc­tion.

How did I fi­nally up­grade to v0.11.0?

Turns out you have to do it the old-fash­ioned way. Go ahead and edit the package.json file, and spec­ify the ver­sion you need. Then in­stall the de­pen­den­cies again.

This is usu­ally frowned upon for good rea­son - this, how­ever, is one ex­cep­tion.