ES Modules and Node.js: Hard Choices

  • Name resolution (relative and absolute URLs and paths)
  • Fetch integration
  • How to describe script tag: <script type=”module”>
  • Memoization / caching

Why does Node.js need ES Modules?

An interoperability proposal for Node.js

  • Avoiding a “boilerplate tax” (e.g. “use module”)
  • Avoiding double-parsing if possible as Modules and Scripts parse differently
  • Don’t make it too difficult for non-JavaScript tools to make the determination (e.g. build toolchains such as Sprockets or Bash scripts)
  • Don’t impose a noticeable performance cost on users (e.g. by double-parsing large files)
  • No ambiguity
  • Preferably self-contained
  • Preferably without vestiges in a future where ES Modules may be the most prominent type

Basic interoperability algorithm

1. Determine if file is an ES Module (ES) or CommonJS (CJS) 
2. If CJS:
2.1. Wrap CJS code to bootstrap code
2.1. Evaluate as Script
2.2. Produce a DynamicModuleRecord from `module.exports`
3. If ES:
3.1. Parse for `import`/`export`s and keep record, in order to create bindings
3.2. Gather all submodules by performing recursive dependency loading
3.3. Connect `import` bindings for all relevant submodules
3.4. Evaluate as Module
{ 
"name": "test",
"version": "0.0.1",
"description": "",
"main": "./index", // no file extension
}
// index.mjs 
export default class Foo {
//..
}
// index.js 
class Foo {
// ...
}
module.exports = Foo;
import mkdirp from 'mkdirp'; 
require('mkdirp');
require('./foo'); 
import './foo';
// these both look at
// ./foo.mjs
// ./foo.js
// ./foo/index.mjs
// ./foo/index.js
// to explicitly load a CJS module, add '.js':
import './foo.js';
// to explicitly load an ES module add '.mjs'
import './bar.mjs';

Examples: Consuming CommonJS with ES Modules

// cjs.js 
module.exports = {
default:'my-default',
thing:'stuff'
};

// es.mjs
import * as baz from './cjs.js';
// baz = {
// get default() {return module.exports;},
// get thing() {return this.default.thing}.bind(baz)
// }
// console.log(baz.default.default); // my-default
import foo from './cjs.js';
// foo = {default:'my-default', thing:'stuff'};
import {default as bar} from './cjs.js';
// bar = {default:'my-default', thing:'stuff'};
// cjs.js 
module.exports = null;
// es.mjs
import foo from './cjs.js';
// foo = null;
import * as bar from './cjs.js';
// bar = {default:null};
// cjs.js 
module.exports = function two() { return 2; };
// es.mjs
import foo from './cjs.js';
foo(); // 2
import * as bar from './cjs.js';
bar.name; // 'two' ( get function name)
bar.default(); // 2 ( assigned default function )
bar(); // throws, bar is not a function

Examples: Consuming ES Modules with CommonJS

// es.mjs 
let foo = {bar:'my-default'};
export default foo;
foo = null; // this null value does not effect import value.
// cjs.js
const es_namespace = require('./es');
// es_namespace ~= {
// get default() {
// return result_from_evaluating_foo;
// }
// }
console.log(es_namespace.default);
// {bar:'my-default'}
// es.mjs 
export let foo = {bar:'my-default'};
export {foo as bar};
export function f() {};
export class c {};
// cjs.js
const es_namespace = require('./es');
// es_namespace ~= {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }

Current state of discussion

  • If package.json has “main” field but not a “module” field, all files in that package are loaded as CommonJS.
  • If a package.json has a “module” field but not “main” field, all files in that package are loaded as ES Modules.
  • If a package.json has neither “main” nor “module” fields, it will depend on on whether an index.js or a module.js exists in the package as to whether to load files in the package as CommonJS or ES Modules respectively.
  • If a package.json has both “main” and “module” fields, files in the package will be loaded as CommonJS unless they are enumerated in the “module” field in which case they will be loaded as ES Modules, this may also include directories.
  • If there is no package.json in place (e.g. require(‘c:/foo’)), it will default to being loaded as CommonJS.
  • A special “modules.root” field in package.json, files under the directory specified will be loaded as ES Modules. Additionally, files loaded relative to the package itself (e.g. require(‘lodash/array’)) will load from within this directory.

In Defense of .js Examples

// package.json 
// all files loaded as CommonJS
{
"main": "index.js" // default module for package
}
// package.json
// default to CommonJS, conditional loading of ES Modules
{
"main": "index.js", // used by older versions of Node.js as default module, CommonJS
"module": "module.js" // used by newer versions of Node.js as default module, ES Module
}
// package.json
// CommonJS with directory exceptions
{
"main": "index.js",
"module": "module.js",
"modules.root": "lib" // all files loaded within this directory will be ES Modules
}

Supporting both CommonJS and ES Modules

Hard choices

Getting involved

NodeSource is the safest and most secure Node.js platform. Secure. Reliable. Extensible. Node.js

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
NodeSource

NodeSource

NodeSource is the safest and most secure Node.js platform. Secure. Reliable. Extensible. Node.js

More from Medium

About Node.js

Why Should I Use Node.js?

Features of Different Javascript Framework : Node Js, BackBone JS, Ext Js

Introduction to Node.js