Skip to main content

Publishing ES6 Modules on NPM

I had an adventure over the last couple days with ES6!
There was a pattern I'd already used in a few of my React projects to make ES6 classes a little nicer. ES6 did a lot to make working with this mechanics nicer, but there was a still a gap that bit me: the sugar provided by ES6 classes don't extend to keeping method bound to instances of the class.
Maybe you, like me, would expect this to work:
class Counter {
    constructor() {
        this.count = 0
    }
    onClick() {
        this.count += 1
    }
}
counter = new Counter()
document.querySelector('#add-button').onclick = counter.onClick
But, like non-class methods on any regular Javascript object, onClick will loose its binding to the Counter instance. There are a few existing solutions to this, but I wanted one that didn't change the syntax of defining a method on these classes.
Enter AutoBind, via my new NPM module es6-class-auto-bind:
import AutoBind from 'es6-class-auto-bind'

class Counter extends AutoBind() {
    constructor() {
        this.count = 0
    }
    onClick() {
        this.count += 1
    }
}
counter = new Counter()
document.querySelector('#add-button').onclick = counter.onClick
You can read all about the AutoBind class [at its NPM readme](https://www.npmjs.com/package/es6-class-auto-bind) and you can read on to learn about what I learned to publish this ES6 module on NPM, consumable by other ES6 (and even ES5) code.

The Problems of ES6 on NPM

For the moment, NPM is a tool for distributing and installing ES5 modules. While you can point it at any types of files you want (some people have even used NPM to distribute C libraries!) the mechanisms that install and then import those modules in NodeJS (or Browserify) are expecting ES5 modules, so they won't do your users any good.
There are two problems we'll face shipping ES6 code directly.
First, most of the ES6 code we might ship would be completely useful for consumption by ES5 code. My choice of ES6 shouldn't prohibit anyone from consuming my libraries. We want to publish something that both new ES6 and legacy ES5 code can make use of without caring much about what's inside. And, we want to do so without carrying build constraints on our users, like requiring they integrate BabelJS into their pipeline when they haven't done so already.
Second, for those consumers of our module who already are using BabelJS or another transpiler to ship their ES6 to ES5 runtimes, importing ES6 code installed by NPM is probably not going to work out of the box! Browserify here is a big culprit, refusing to apply configured transform plugins to packages installed from node_modules/, only to those from your own local project.
Now, I understand Webpack may be better about enabling this usecase, but I don't want to impose that move to people still on Browserify (and I still want to support ES5 users), so I wanted a solution that works for everyone.

How To Combine Packaging and Transformation

The solution is to tranform our ES6 module to ES5 before publication, and idealy to automate this. We want to transform it into an ES5 version of itself and tell NPM to publish that version of our module, instead of the original ES6 version. Here's how we do it.
We'll put our two versions into a src/index.js and build/index.js. Transforming the first to the second is straightforward with BabelJS, which we'll install first:
npm install --save-dev babel-cli
npm install --save-dev babel-preset-es2015
npm install --save-dev babel-runtime
node_modules/.bin/babel src/index.js > build/index.js
Now we have both versions, and we only need to tell NPM what we actually want a consumer to get when they require()or import it.
"main": "./build/index.js",
Great! But we still need to make this happen automatically any time we issue an npm publish, never allowing us to publish a version that isn't compiled from the most recent version of the ES6 source.
"scripts": {
    "compile": "node_modules/.bin/babel src/index.js > build/index.js",
    "prepublish": "npm run compile"
}
We've defined two npm run scripts now: compile and prepublish. We can run npm run compile to test our preparation any time, and NPM itself will invoke prepublish before any new version you attempt to upload via npm publish. We've now configured our module to transform from ES6 to ES5 before publication to NPM, where it is consumable by any other project that needs it!
We're almost done at this point. There is a last step we can take to make the whole process more consistent between ES5 and ES6 norms. The ES6 module syntax's export statement is largely comparable to exports.member = somethingstatements in NodeJS' ES5 modules, and BabelJS will transform them appropriately. But export has a special form for exporting one member as a default, to be handed to an importing module when it only asks for a single thing from the module.
import AutoBind from 'es6-class-auto-bind'
export default class AutoBind {
The problem is BabelJS transforms this by exporting these defaults with the obvious name "default", and accesses the .default member of a module when performing a default import. But, this means ES5 code would need to access the .default member explicitly, with the unfortunate requires() invoking as require("es6-class-auto-bind").default. We'd like to get rid of that ugly .default at the end, obviously.
It turns out this is a behavior BabelJS did have but changed. It is also a behavior we can restore through a plugin that re-implementes the deprecated behavior. I think allowing it to be optional like this is just fine. We just need to install the plugin
npm install --save-dev babel-plugin-add-module-exports
And change our compile script to enable the plugin
"scripts": {
    "compile": "node_modules/.bin/babel src/index.js --plugin add-module-exports > build/index.js",
    "prepublish": "npm run compile"
}
And, that's it. Everything works great now. This is how I was able to ship my ES6 AutoBind class via NPM and install into other ES6 classes, seamlessly building my ES6 code across packages. Very exciting!
Here's the whole portion of the package.json necessary to make this work.
"main": "./build/index.js",
"scripts": {
    "compile": "node_modules/.bin/babel --plugins add-module-exports src/index.js > build/index.js",
    "prepublish": "npm run compile"
},
"devDependencies": {
    "babel-cli": "^6.7.5",
    "babel-plugin-add-module-exports": "^0.1.2",
    "babel-preset-es2015": "^6.6.0",
    "babel-runtime": "^6.6.1",
Stay subscribed for follow up posts on the subject, as I dig into how to expand this to:
  • Ship a copy of the ES6 code in parallel and pull that into the project's own transform options
  • Understand how to expand this approach to packages with more than one module

Comments

Popular posts from this blog

CARDIAC: The Cardboard Computer

I am just so excited about this. CARDIAC. The Cardboard Computer. How cool is that? This piece of history is amazing and better than that: it is extremely accessible. This fantastic design was built in 1969 by David Hagelbarger at Bell Labs to explain what computers were to those who would otherwise have no exposure to them. Miraculously, the CARDIAC (CARDboard Interactive Aid to Computation) was able to actually function as a slow and rudimentary computer.  One of the most fascinating aspects of this gem is that at the time of its publication the scope it was able to demonstrate was actually useful in explaining what a computer was. Could you imagine trying to explain computers today with anything close to the CARDIAC? It had 100 memory locations and only ten instructions. The memory held signed 3-digit numbers (-999 through 999) and instructions could be encoded such that the first digit was the instruction and the second two digits were the address of memory to operat...

Statement Functions

At a small suggestion in #python, I wrote up a simple module that allows the use of many python statements in places requiring statements. This post serves as the announcement and documentation. You can find the release here . The pattern is the statement's keyword appended with a single underscore, so the first, of course, is print_. The example writes 'some+text' to an IOString for a URL query string. This mostly follows what it seems the print function will be in py3k. print_("some", "text", outfile=query_iostring, sep="+", end="") An obvious second choice was to wrap if statements. They take a condition value, and expect a truth value or callback an an optional else value or callback. Values and callbacks are named if_true, cb_true, if_false, and cb_false. if_(raw_input("Continue?")=="Y", cb_true=play_game, cb_false=quit) Of course, often your else might be an error case, so raising an exception could be useful...

How To Teach Software Development

How To Teach Software Development Introduction Developers Quality Control Motivation Execution Businesses Students Schools Education is broken. Education about software development is even more broken. It is a sad observation of the industry from my eyes. I come to see good developers from what should be great educations as survivors, more than anything. Do they get a headstart from their education or do they overcome it? This is the first part in a series on software education. I want to open a discussion here. Please comment if you have thoughts. Blog about it, yourself. Write about how you disagree with me. Write more if you don't. We have a troubled industry. We care enough to do something about it. We hark on the bad developers the way people used to point at freak shows, but we only hurt ourselves but not improving the situation. We have to deal with their bad code. We are the twenty percent and we can't talk to the eighty percent, by definition, so we need to impro...