The need for multi-platform npm packages

[2016-03-20] dev, javascript, esnext, npm, jsmodules
(Ad, please don’t block)

In this blog post, I argue that it should be possible to have multiple implementations of the same npm package (same name, same version).

The problem  

At the moment, when you write an npm package, you can specify on what platforms it works, via the package.json property engines. For example:

{ "engines" : { "node" : ">=0.10.3 <0.12" } }
{ "engines" : { "npm" : "~1.0.20" } }

That means that you can only have a single implementation per package. However, there are use cases for multiple implementations of the same package:

  • For Node.js you can already use many ES6 features. For browsers, you should stay 100% ES5.
  • There are Node.js-specific polyfills of Browser APIs. For example, node-fetch polyfills the fetch API.
  • The module bundler Rollup needs the ES6 module format to achieve its superior file size savings. But that format doesn’t work anywhere else, yet.

An idea for a solution  

I see two possible solutions:

  • Allow the same package (same name, same version) to exist multiple times, targeting different platforms via property engines.
  • Allow packages with multiple versions of the properties main and bin.

The latter solution could lead to package.json files that look like this:

"engines": [
    {
        "node": ">=0.10.3 <0.12",
        "main": "./es5/index.js",
        "bin": { "foo": "./es5/bin/foo.js" }
    },
    {
        "ecmascript": ">=2015",
        "main": "./es2015/index.js",
        "bin": { "foo": "./es2015/bin/foo.js" }
    }
],

Mixing selection criteria (meta-data) and data is not ideal. This is an alternative:

"engines": {
    "node >= 0.10.3, node < 0.12": {
        "main": "./es5/index.js",
        "bin": { "foo": "./es5/bin/foo.js" }
    },
    "ecmascript >= 2015": {
        "main": "./es2015/index.js",
        "bin": { "foo": "./es2015/bin/foo.js" }
    }
},

The selection criteria should include:

  • Browsers vs. Node.js
  • Supported ECMAScript version
  • Module format: native (ES6) vs. CommonJS vs. AMD

Other solutions  

Ecosystems  

I have only seen a brief mention of npm ecosystems, so far. I’m not sure how exactly they would work, but it sounds like they could solve the problem I’ve described here.

jsnext:main  

jsnext:main is a custom property that Rollup uses to point to an ES6 module version of the main file. The problem with this approach (apart from the less-than-ideal property name) is that it can only handle a single alternate implementation with a fixed format.

More information on jsnext:main:

jspm  

The package manager jspm extends package.json with, among others, the property format whose value can be esm (for ECMAScript module), amd, cjs or global.

Additionally, you have the option to nest jspm-specific properties via the custom property jspm. For example:

{
    "name": "my-package",
    "jspm": {
        "main": "jspm-main"
    }
}

More information: “Configuring Packages for jspm”.

Feedback?  

Feedback welcome! Did I miss anything? Are other (better?) solutions out there?