What is the minimum version of Node your NPM module requires? You might think it is Node 8 or Node 10 or even Node 6. But in reality you don't know - because the direct or transient NPM dependencies your project uses might require a different higher version, without even declaring it explicitly in the engines
field of their package.json
files.
Recently, we have experienced sudden "bumps" in minimum Node version required for our Cypress NPM package because of chalk and execa dependencies. While we promised supporting Node v8.0.0, due to these dependencies our true minimum Node version turned out to be v8.12.0!
The only reliable way to determine if your project runs on Node 8.0.0 is to run your NPM package on Node 8.0.0. In this blog post I will show how to bundle and transpile your NPM package so it truly runs on Node v8.0.0 or even on Node v6.
Note: you can find the source code at bahmutov/support-node-v6
The problem
Let's start a new NPM application that uses chalk
and execa
to print the list of files. We want to use the latest versions of all the tools, so we are working on Node v12
1 | ~/git/support-node-v6 on master |
Here is our application file index.js
1 | const chalk = require('chalk') |
The app runs and the colors show up (on Node v12)
Now let's try the same application on Node v6.
Because chalk
uses spread operator, it does not run on Node v6 - and you would need to downgrade to [email protected]
. But then the same happens with execa
- it also uses the spread operator!
You would need to downgrade execa
all the way to v1 to get the syntax compatible with Node v6. In the process you just lost soooo many features and fixes from execa
and chalk
, it is almost sad.
Bundling
Let's switch tactics. Instead of searching for an old version of chalk
and execa
, let's install the latest versions - and let's bundle them into a single JavaScript file. Typically, bundling is done for browsers, but we will use @zeit/ncc to bundle for Node.
1 | $ npm i -S chalk execa |
We can use ncc
to produce a single JavaScript file from index.js
entry using npm run build
script
1 | { |
1 | $ npm run build |
The bundle we produced does not run on Node 6 yet.
But the bundle has all dependencies included. We can remove node_modules
folder and run it to prove this.
Now we can transpile this single file to make it work on (almost) any Node version.
Transpiling down
I like using TypeScript compiler to transpile code.
1 | { |
This almost works.
The bundle is transpiled - the spread operators in chalk
and execa
have been replaced, but the bundle still has Object.entries
method that Node v6 does not understand. We can polyfill this method though.
1 | $ npm i -S babel-polyfill |
Then require the polyfill from index.js
file
1 | require('babel-polyfill') |
Let's build and transpile again.
Nice, the latest dependencies do work on Node v6!
Tips
Run scripts
Since we only plan to distribute a single bundle, we can generate the intermediate bundle in the build
folder, and run the transpile step post-build.
1 | { |
Specify bundled dependencies
We can see the produced bundle using npm pack --dry
command.
1 | $ npm run build |
The zipped archive has size 114 kB, which looks like a lot. But remember, that anyone installing this NPM app should only download this single file, which should be as fast as downloading multiple production dependencies. But we still list execa
and chalk
as production dependencies, thus our users will get two copies of them - the second one coming from the dist/index.js
bundle.
Hmm, NPM understands bundled dependencies list, but this list forces chalk
and execa
into the TGZ archive twice!
1 | { |
1 | $ npm pack --dry |
Thus my advice is to move all production dependencies into devDependencies
- they will be included in the bundle as needed and skip using bundledDependencies
.
1 | { |
Let's make sure the bundled dependencies work. I have created a new NPM package in a different folder and will install and run the above project using Node v6.
Perfect.
Source maps
While @zeit/ncc
and typescript
can both generate source maps, I could not find a way to connect the two to get the source maps to link an error back to the original source file. If you know how to do this, open a pull request in bahmutov/support-node-v6, please.
Unsupported features
Some ES6+ syntax and features cannot be transpiled down, for example if your project requires WeakMaps or Proxies - you are out of luck.
alternative: use Parcel
Instead of @zeit/ncc
we can use Parcel bundler to bundle code like this:
1 | { |