This post is part of a series of three:

Current approaches: “Setting up multi-platform npm packages” Motivating a new approach: “Transpiling dependencies with Babel” Implementing the new approach: “Delivering untranspiled source code via npm”

This blog post explains ways of targeting multiple platforms via the same npm package.

Before we get into the actual topic, let’s quickly review common JavaScript module formats.

This following table gives an overview of standard properties in package.json that are used for pointing to source code.

main browser module es2015 ES version ES5+ ES5+ ES5+ ES6 Module format CJS CJS ESM ESM webpack ✔ ✔ ✔ – Rollup (✔) (✔) ✔ – jspm ✔ – – –

Note: “ES5+” means “whatever language features are supported by the JavaScript engines you are targeting”.

Background: JavaScript module formats #

At the moment, these are the most common JavaScript module formats:

AMD (asynchronous module definition): an asynchronous module format for browsers.

CJS (CommonJS): a synchronous module format designed for servers (such as Node.js). Due to the popularity of Node.js and npm, CJS has become the most widely used format for browsers, too. But it has to be compiled to asynchronous code there. Tools that do that include webpack and Browserify.

ESM (ECMAScript modules): With ES6, modules became a built-in part of JavaScript. ESM modules are designed to work both synchronously and asynchronously, enabling them to be a universal module format. Support for ESM in browsers is slowly appearing. Support in Node.js is work in progress and estimated to be production-ready by early 2018 (preliminary support may appear earlier).

UMD (Universal Module Definition) #

The idea of UMD is that you can implement a JavaScript module in such a manner that it supports (up to) three formats at the same time: AMD, CJS and delivery via a global variable.

This is a UMD module that supports AMD and CJS (source):

( function ( define ) { define( function ( require, exports, module ) { var b = require ( 'b' ); return function ( ) {}; }); }( typeof module === 'object' && module .exports && typeof define !== 'function' ? function ( factory ) { module .exports = factory( require , exports, module ); } : define ));

Documentation:

“UMD (Universal Module Definition) patterns for JavaScript modules” by Addy Osmani, Evan Carroll, Anders D. Johnson, James Burke and others

“When sniffing for exports, make sure exports is not an HTMLElement” by Chris Dickinson explains how to best detect if Node.js is running (e.g.: checking whether require is a function may not work if require.js is being used).

The problem #

You can only deliver source code for a single platform via an npm package. Property engines of package.json lets you specify exactly what platform that is:

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

However, that doesn’t help you with the following use cases, where you need source code for multiple platforms per package:

Browsers: deliver both a native version (e.g. in ES5 via CJS) and a “bleeding edge” version (e.g. latest ECMAScript version via an ES module), to be transpiled by Babel. Node.js: deliver the same module for several versions of Node.js. Browsers: allow new libraries to age gracefully – transpile only as long your target platforms don’t support the features, yet. We want the same convenience that babel-preset-env affords us.

There are two dimensions at play here:

On one hand, there is a distinction between code that is to be transpiled and “native” code.

On the other hand, native code may have to run on platforms with different capabilities.

The next section covers solutions for use case 1.

The following subsections explain properties in package.json that can be used to point to alternate versions of the same code.

When I use the term “native features”, it means: language features supported by the platforms you are targeting.

main : native features, CJS #

main is the standard mechanism for pointing to the module code inside a package if you want to override the default path, index.js . It is supported everywhere. This is an example:

{ "name" : "the-package" , "version" : "1.0.1" , "main" : "dist/the-package.umd.js" , }

module : native features, ESM #

This property helps tools such as the tree-shaking module bundler Rollup that depend on the ESM format. Other than that, only native language features are supported. That is, module is just main with a different module format:

{ "name" : "the-package" , "version" : "1.0.1" , "main" : "dist/the-package.umd.js" , "module" : "dist/the-package.es2015.js" }

Documentation:

“pkg.module” by Rich Harris

es2015 : ES6, ESM #

Angular v4 delivers each package in three formats:

UMD: via property main

ES5/ESM: via property module

ES6/ESM: via property es2015

This is what its package.json looks like:

{ "name" : "@angular/core" , "main" : "./bundles/core.umd.js" , "module" : "./@angular/core.es5.js" , "es2015" : "./@angular/core.js" , ··· }

I like the idea of this property. But its name and semantics mean that it’ll age relatively quickly.

Documentation:

“Angular 4.0.0 Now Available” by Stephen Fluin

jsnext:main : the precursor of module #

The property jsnext:main is now deprecated. It was superseded by module .

browser : browser-specific code #

The idea of the property browser is that:

main provides Node.js code

provides Node.js code browser provides browser-specific code

The simplest mode of browser is as an alternative to main :

{ "main" : "dist/the-package.server.js" , "browser" : "dist/the-package.client.js" , ··· }

An advanced mode lets you replace specific files:

"browser" : { "module-a" : "./shims/module-a.js" , "./server/only.js" : "./shims/client-only.js" }

Documentation:

“package-browser-field-spec” by Roman Shtylman

Support by bundlers #

main browser module es2015 webpack ✔ ✔ ✔ – Rollup (✔) (✔) ✔ – jspm ✔ – – –

Comments:

webpack lets you configure where it looks for source code (see next section), so getting it to support es2015 is simple.

is simple. Rollup specializes in the ESM format. If you want it to handle CJS modules, you need a plugin.

jspm has its own configuration mechanisms (property jspm and others).

For webpack, you can configure where it searches for module code inside packages via the resolve.mainFields option:

module .exports = { ··· target: "web" , resolve: { mainFields: ···, ··· }, ··· }

The default value of this property depends on the value of target .

If target is "web" , "webworker" or unspecified then the default is:

mainFields: [ "browser" , "module" , "main" ]

If target has any other value (including "node" ) then the default is:

mainFields: [ "module" , "main" ]

Documentation:

“ resolve.mainFields ” in the webpack documentation

Support for multi-platform packages has come a long way. The main challenge ahead is to make sure transpiling external dependencies is as “auto-updating” and hassle-free as babel-preset-env .

Further reading #