Skip to content

Bundling multiple ES6 modules

Rich Harris edited this page Apr 7, 2015 · 14 revisions

If you're building a library or application using ES6 modules, the likelihood is that you'll have multiple files that depend on each other, plus various third party dependencies that aren't ES6 modules.

If it's a library, you'll also need to export some functions in such a way that they can be used by other developers.

Esperanto lets you bundle your ES6 modules into a single file in one of three formats:

  • AMD (asynchronous module definition) - works with e.g. RequireJS
  • CommonJS - works in node.js or with Browserify
  • UMD (universal module definition) - works as an AMD module, a CommonJS module, or as a browser global

It's a bit like Browserify or the RequireJS optimizer, except that the end result is more compact, and you can use ES6 module features like cycles and bindings. You can import external dependencies, and export functions and values from your bundle.

If you don't have any external dependencies, you can create a self-executing bundle with bundle.concat() (see below).

Example scenario

Suppose you're creating a library, mean.js, that calculates the mean value of an array of numbers:

mean
|- dist
|- src
   |- math-utils.js
   |- mean.js
|- package.json
|- README.md

The src files look like this:

mean.js

import { total } from './math-utils';

export default function ( arr ) {
  if ( !arr.length ) return 0;
  return total( arr ) / arr.length;
}

math-utils.js

import assert from 'assert'; // node's builtin assert module

export function total ( arr ) {
  return arr.reduce( function ( total, num ) {
    assert.equal( typeof num, 'number' ); // error otherwise
    return total + num;
  }, 0 );
}

We want to bundle both files into a single CommonJS module:

var fs = require( 'fs' );
var esperanto = require( 'esperanto' );

esperanto.bundle({
  base: 'src', // optional, defaults to current dir
  entry: 'mean.js' // the '.js' is optional
}).then( function ( bundle ) {
  var cjs = bundle.toCjs();
  fs.writeFile( 'dist/mean.js', cjs.code );
});

Esperanto will start at the entry module (mean.js), and trace all the dependencies until there are no more (rather easy in this case):

  • Relative paths (e.g. './math-utils') are relative to the importer, otherwise modules are resolved against the base directory.
  • foo/index.js is equivalent to foo.js
  • Any modules that can't be resolved are kept as external dependencies

The end result is a CommonJS module like this:

(function () {
  'use strict';

  var assert = require( 'assert' );

  function total ( arr ) {
    return arr.reduce( function ( total, num ) {
      assert.equal( typeof num, 'number' ); // error otherwise
      return total + num;
    }, 0 );
  }

  module.exports = function ( arr ) {
    if ( !arr.length ) return 0;
    return total( arr ) / arr.length;
  };
}).call(global);

Note that the default export of mean.js becomes module.exports - if you want to futz around with named exports from your entry module, use strict: true...

var cjs = bundle.toCjs({ strict: true });

...but read the page on strict mode first!

## Other formats and options

As well as bundle.toCjs(), you can use bundle.toAmd() or bundle.toUmd(), just as with one-to-one transformations. In the UMD case, you must supply a name option.

transform

You can pass a transform option to esperanto.bundle(), and your modules will be transformed prior to bundling:

esperanto.bundle({
  base: 'src',
  entry: 'main.js',
  transform: function ( source ) {
    // we can return a string, or a Promise
    // that resolves to a string
    return removeTypeAnnotations ( source ); // for example...
  }
}).then(...);

modules

By default, Esperanto will read files from disk. If you would rather supply the source code directly, you can:

esperanto.bundle({
  base: 'src',
  entry: 'main.js',
  modules: {
    // keys are filenames, relative to `base` (or
    // `process.cwd()`, if base is omitted
    'main.js': 'import foo from "./utils/foo";',

    // advanced: if you already have an ESTree AST,
    // you can supply that
    'utils/foo.js': {
      code: '...',
      ast: {...}
    }
  }
}).then( ... );

skip

You can prevent modules (and their dependencies, if they're not used elsewhere in your bundle) from being included by using the skip option. For example, you might have a version of your library that doesn't need to support IE8, so you can skip the polyfills:

esperanto.bundle({
  base: 'src',
  entry: 'main',
  skip: [ 'polyfills' ]
}).then(...);

Self-executing bundles

If you're using ES6 modules to organise your code, but don't have any external dependencies and don't need to make the bundle module-loader-friendly (i.e. you're writing an app, rather than a library, for example), you can create a self-executing bundle with bundle.concat():

esperanto.bundle( options ).then( function ( bundle ) {
  var selfExecuting = bundle.concat();
  fs.writeFileSync( 'app.js', selfExecuting.code );
});

Any libraries that your bundle depends on must exist in the global namespace (i.e. be on the page as a <script> tag) before the bundle executes.

Getting information about the bundle

Imports and exports

As well as bundle.toAmd() and similar methods, the bundle object has imports and exports properties. imports is an array of dependencies external to the bundle, and exports is an array of names (which might include 'default') exported by the bundle's entry module:

// tick.js
import _ from 'lodash';
export default function tick () {
  console.log( 'the time is %s', _.now() );
}

// main.js
import tick from './tick';

var interval;

export function start () {
  interval = setInterval( tick, 1000 );
}

export function stop () {
  clearInterval( interval );
}

// build script
esperanto.bundle({
  main: 'main.js'
}).then( function ( bundle ) {
  console.log( bundle.imports ); // [ 'lodash' ]
  console.log( bundle.exports ); // [ 'start', 'stop' ]
});