You can use a lot of ES6 features today. Most of the features are already implemented in Node 4, 5 and even 0.12. You can precisely transpile the missing features when the client installs your module, see my blog post JavaScript needs the compile step (on install). The ES6 features already supported by the client are unchanged, and only the missing features are transpiled.
There was a lot of feedback to the blog post and the example implementation, ranging from "No way, don't do this" to "Wow, this is the way JavaScript should be written in 2016". A lot of people pointed the major shortcoming of the proposed method: it adds a LOT of weight to the NPM install - the client has to download Babel and its plugins in order to transpile!
I wanted a better solution.
Pre-compiled JavaScript
Here is an alternate solution. The range of NodeJS versions in the wild is pretty small. The most popular are 0.10, 0.12, and the newer versions 4 and 5. We can pre-build a single bundle for each of these versions during the build step (on the dev or CI machine). During the installation by the client, we can determine which bundle to use depending on the Node version.
This obviously increases the NPM download size, but the increase is only in the source code size, and not in any additional module downloads.
I wrote pre-compiled tool for the devs. The tool
uses compiled to bundle (using Rollup) and transpile
the produced code for different Node platforms. I have collected the ES features available by
default (without --harmony
flag) on each platform, see
features files. I still test
the source code to see which ES features it actually uses to minimize transpile times and avoid
transpiling a feature the platform already supports.
Example
Here is an example how one could use this approach in practice. I have created precompiled-example repo with a few source files. It uses my favorite ES6 features, for example template literals and object parameter shorthand notation
1 | import { add } from './calc' |
It uses ES6 module import
of course to allow efficient tree-shaking when producing the
output bundle.
First, install the pre-compiled build tool
npm install --save-dev pre-compiled
Create the build step
1 | "scripts": { |
We need to tell pre-compiled
tool which files to start with and where to output them.
Add a config
object to the package.json
file
1 | "scripts": { |
The precompile
step will start with src/main.js
file and will roll all ES6 'imported' modules
into one bundle, producing dist/main.js
bundle. Then it will produce several bundles
(you can control this list via config option, of course)
dist/main.compiled.for.0.10.js
dist/main.compiled.for.0.11.js
dist/main.compiled.for.0.12.js
dist/main.compiled.for.4.js
dist/main.compiled.for.5.js
We should add these bundles to the list of files included in our NPM package
1 | { |
Second, we need to add a production dependency that will run on the client during install.
npm install --save pick-precompiled
And we need to call it during postinstall
step
1 | "scripts": { |
We have multiple bundles, and at install one of them will be picked and copied to dist/main.js
(according to the output directory and the original bundle name). Thus we should also set
the main script to point at the bundle, even if it does not exist yet.
1 | { |
Picking bundle in action
I have made a precompiled-example module to show the bundling and picking the right bundle in action.
If we do npm install precompiled-example
we get the following output
node version 0.12.9
for node version 0.12.9 picked bundle dist/main.compiled.for.0.12.js
copied bundle dist/main.js
Our bundle has a lot of transpiled features and it works
$ node node_modules/precompiled-example/
Adding object properties 12
binary literal 5
object foo and bar
10 + 2 = 12
We can inspect the bundle, notice the transpiled features for Node 0.12
$ cat node_modules/precompiled-example/dist/main.js
'use strict';
require('pick-precompiled').babelPolyfill()
var add = function (a, b) {
return a + b;
};
var a = 10;
var b = 2;
Promise.resolve(add(a, b)).then(function (sum) {
console.log(a + ' + ' + b + ' = ' + sum);
});
var objectAdd = function (_ref) {
var a = _ref.a;
var b = _ref.b;
return a + b;
};
console.log('Adding object properties', objectAdd({ a: 10, b: 2 }));
...
Let us try installing from Node 4.
$ nvm use 4
Now using node v4.2.2 (npm v3.5.0)
for node version 4.2.2 picked bundle dist/main.compiled.for.4.js
copied bundle dist/main.js
$ node node_modules/precompiled-example/
Adding object properties 12
binary literal 5
object foo and bar
10 + 2 = 12
Same working module, but the code now runs without transpiled parts, Node 4 supports almost all features we needed (arrows, template literals)
$ cat node_modules/precompiled-example/dist/main.js
'use strict';
require('pick-precompiled').babelPolyfill()
const add = (a, b) => a + b;
const a = 10;
const b = 2;
Promise.resolve(add(a, b)).then(function (sum) {
console.log(`${ a } + ${ b } = ${ sum }`);
});
const objectAdd = _ref => {
let a = _ref.a;
let b = _ref.b;
return a + b;
};
console.log('Adding object properties', objectAdd({ a: 10, b: 2 }));
Finally, let us see if Node 0.10 is working.
$ nvm use 0.10
Now using node v0.10.40 (npm v1.4.28)
$ npm install precompiled-example
for node version 0.10.40 picked bundle dist/main.compiled.for.0.10.js
$ node node_modules/precompiled-example/
Adding object properties 12
binary literal 5
object foo and bar
10 + 2 = 12
Nice! We do have a nice bundle for each case.
Conclusion
With this approach you can admit that your NPM module is pre-rolled and pre-compiled :)
There are shortcomings - building and including several bundles instead of one, changing Node
version using nvm
requires npm install
call. But overall I like this approach because the
bundles installed have nice, clean source.