This blog was written by Benjamin Coe. Ben works on the open-source libraries yargs, nyc, and c8, and is a core collaborator on Node.js. He works on the client libraries team at Google. This piece originally appeared on the Node.js Collection. Node.js is an impact project of the OpenJS Foundation.
tldr; Node.js has a tried and true release schedule, supporting LTS versions for 30 months. It offers significant benefits to the community for library maintainers to follow this same schedule:
- ensuring the ability to take security patches.
- reducing the burden on maintainers.
- allowing module authors to take advantage of new platform features sooner.
My opinion of what Node.js versions library maintainers should aim to support has evolved over the years. Let me explain why…
I joined npm, Inc in April 2014. During this period, releases of Node.js had stalled. Node.js
v0.10.x was released in April 2013, and Node.js
v0.12.x wouldn’t be released until February 2015.
At the same time, the npm package registry was going through growing pains (see: “Outage Postmortem”, “Four hours of partial outage”, etc.).
The state of Node.js and npm in 2014 had side effects on how folks thought about writing libraries: maintainers didn’t need to put mental overhead into deciding what Node.js versions they supported (for years, the answer was
0.10.x); partially owing to npm’s instability, and partially owing to frontend communities not having fully embraced npm for distribution, package dependency trees were smaller.
Small building blocks, like mkdirp, still represented a significant portion of the registry in 2014.
Things would change in the intervening six years…
In February of 2015, motivated by the io.js fork, The Node.js Foundation was announced. In September of that same year, Node.js
v4.0.0 was released. Node.js
v4.0.0 merged the io.js and Node.js projects unblocked the release logjam and introduced the 30-month LTS cycle advocated in this article.
In parallel, during the period between 2014 and today, npm significantly improved stability, and the frontend community began to consolidate on npm for distribution. The side effect was that many more packages were being published to npm (numbers grew from 50,000 in 2014, to 700,000 in 2018). At the same time, dependency trees grew (the average number of dependencies in 2016 was 35.3, the average number of dependencies in 2018 was 86).
A library maintainer in 2020 has at least three versions of Node.js to think about (the Current, Active, and Maintenance versions). Also, on average, their libraries rely on an increasing number of dependencies… it can be a bit daunting!
A library maintainer in 2020 has at least three versions of Node.js to think about (the Current, Active, and Maintenance versions).
Here’s how adopting this schedule benefits both the library authors and the community…
Being able to take security patches
A security vulnerability was recently reported for the library minimist. minimist is the direct dependency of 14,000 libraries… it’s a transitive dependency of the universe.
The popular templating library Handlebars was bitten by this report through an indirect dependency (optimist). Handlebars was put in a position where it was difficult to silence this security warning without disrupting its users:
- optimist, deprecated several years earlier, was pinned to an unpatched version
(~0.0.1) of minimist.
- Handlebars itself supported Node.js
v0.4.7, making it a breaking change to update to yargs (optimist’s pirate-themed successor).
Although motivated by good intentions (“why not support as many environments as possible?”), when libraries support end-of-life versions of Node.js, it can ultimately end in disruptions for users. Maintainers find themselves bumping a major version as a fire drill, rather than as a scheduled update.
Dropping support for old @nodejs release is a breaking change and it should be released in a major version.. — Matteo Collina
The wide adoption of Node.js’ LTS schedule for modules ensures that security patches can always be taken.
Reducing the burden on maintainers
Keeping dependencies up to date is a lot of work (my team at Google landed 1483 pull requests updating dependencies last month), it’s also important:
- the closer to a dependency’s release you catch an unintended breakage, the more likely it will be quickly fixed or rolled back.
- keeping dependencies fresh helps ensure that critical vulnerabilities and bug fixes are rolled out to your own users (this avoids the Handlebars/minimist issue discussed).
Tools like Dependabot and Renovate make sure updating dependencies isn’t a maintainer’s full-time job. However, if libraries don’t adhere to the same version support policy, it makes automation difficult. As an example, because of falling behind the scheduled deprecation of Node.js
v8.x.x, the library yargs turned off automatic updates for decamelize (opening itself up to all the risks that go along with this).
A lot of open-source is made possible by the volunteer work of maintainers. I can’t think of many things less exciting than the constant auditing of the SemVer ranges advertised in the “engines” fields of dependencies.
The wide adoption of Node.js’ LTS schedule for modules creates consistency and reduces the maintainer burden around updating dependencies.
Helping to evolve the platform
For the last couple of years, I’ve been involved in the Node.js Tooling Group. We’ve advocated for a variety of API improvements for tooling authors, such as recursive directory creation, recursive directory removal, and Source Map support for stack traces.
v8.4.0, http2 support was added. This addition is near and dear to my heart since it allows Google’s client libraries (which rely on HTTP/2) to run natively on Node.js.
Keeping the Node.js core small will always be an architectural goal of the project. Node.js is, however, an evolving platform.
The wide adoption of Node.js’ LTS release schedule allows module authors to leverage exciting new features that are added to the platform.
What actions am I advocating that library maintainers take?
- When you release a new library, set the engines field to the oldest active LTS version of Node.js.
- When a Node.js version reaches end-of-life, even if your library is finished, bump a major version updating the engines field to the current oldest active LTS of Node.js.
- Consider throwing a helpful exception if your library is used on an unsupported Node.js version.
- Consider documenting your version support policy (here’s an example of the one we wrote for my team).
The Node.js Package Maintenance Working Group is developing further recommendations related to library support policies, such as Cloud Native’s Long Term Support for Node.js Modules. This policy takes this article’s recommendations a step further and suggests that module maintainers support a major library version for the lifetime of the Node.js runtime it was released under. This means that, if your library releases
v1.0.0 with support for Node
v10.x.x, you would continue to backport security and bug fixes to
v1.0.0 until Node
v10.x.x reaches end-of-life. Committing to this level of support can be a great move for libraries targeting enterprise users (and, they even have a badge!).
Tracking Node.js’ release schedule saves you time, makes libraries more secure, allows you to use Node.js’ fancy new features, and benefits the community as a whole. Let’s get good about doing this together.