Skip to content
Rich-Harris edited this page Nov 12, 2014 · 4 revisions

What is strict mode, and when should I use it?

Interoperability with AMD and CommonJS is hard

ES6 modules support both default and named imports and exports:

// default
import foo from 'foo';
var bar = foo.toUpperCase();
export default bar;

// named
import { foo, bar } from 'baz';
var qux = ( foo + bar ).toUpperCase();
export { qux };

See jsmodules.io for an explanation of the difference.

This is a good design, but it poses problems for developers who want to use ES6 modules with existing codebases. A CommonJS representation of the first example above might look like this:

var foo = require('foo').default;
var bar = foo.toUpperCase();
exports.default = bar;

As long as foo is also a transpiled ES6 module with a default property (or a CommonJS module that had a default property added by design), that's fine - as far as this module is concerned. But if foo is an external library, that almost certainly won't be the case.

On the other side of the fence, if someone were to require this converted ES6 module from within another CommonJS module, they'd have the same problem in reverse - they'd have to know that they needed to append .default to all their require() statements.

The reasons for this design decision are good, but it poses a huge problem for interoperability. No matter how well designed ES6 modules are, if no-one can use modules from the npm ecosystem inside their ES6-authored apps - or if libraries written with ES6 modules can't be used by anyone else - then they'll continue to be a 'standard' in name only.

Interoperability is a top priority for Esperanto.

The solution

Some of the brightest minds in the industry have wrestled with interoperability. See these threads! There are many open issues on many different projects attempting to tackle this problem.

Most of the proposed solutions introduce a lot of complexity. But that complexity melts away if - for the purposes of interoperability - you sacrifice named imports and exports at the boundaries of your code.

Esperanto would transpile the module above like so:

var foo = require('foo');
var bar = foo.toUpperCase();
module.exports = bar;

(Actually, that's not true - it adds a use strict pragma and wraps it in an IIFE bound to the global scope, because that's what the ES6 spec mandates. But you get the idea.)

We can now publish this ES6-authored module, and it will work seamlessly with existing code, without forcing other people to use a particular module loader, or making it necessary to gain industry-wide agreement on which clever hack we're going to use.

But I want to use named imports and exports!

You can! Within a bundle, there are no restrictions. Let's say we're building a library that calculates the mean value of an array of numbers:

// 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 );
}

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

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

Here, the math-utils module has a named export, total, which the main module, mean, imports. We can bundle this up into a single file, using the node API:

esperanto.bundle({ entry: 'mean.js' }).then( function ( bundle ) {
  var transpiled = bundle.toCjs();
  fs.writeFile( 'built/mean.js', transpiled );
});

Because the bundler couldn't find a file called assert.js, it keeps it as an external reference. So the end result looks something like this:

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;
};

So you can happily use everything good about ES6 modules inside your code - the restriction only comes into play at the boundaries of your code.

So... what about strict mode?

Strict mode is for when you don't want even that minimal restriction, and don't particularly care about interoperability. With strict mode enabled, you can use named imports and exports everywhere - but consumers of your code must remember to do...

var mean = require( 'mean' ).default;

...rather than

var mean = require( 'mean' );

Note that default imports from other CommonJS/AMD modules will still generally work - there's a runtime check that goes something like this:

var assert = require( 'assert' );
var assert__default = ('default' in assert ? assert.default : assert);

Any references to assert in your code would be replaced with assert__default.