DEV Community

Hugo Di Francesco
Hugo Di Francesco

Posted on • Originally published at codewithhugo.com on

Use ES modules in Node without Babel/Webpack using `esm`

Node has been implementing more and more ES6+ (ESNext) features natively. One of the features that is taking the longest to implement is modules. The reason for this is that Node and npm run on what is called CommonJS, with which you use require('module-name') to import from other modules and use the module.exports object to expose entities from a module.

Node’s CommonJS was actually one of the first widely adopted module systems in JavaScript. The ease with which one can bundle CommonJS coupled with its widespread use in Node applications and tools means CommonJS quickly displaced RequireJS and SystemJS for frontend application dependency and module management

CommonJS has some drawbacks, like being hard to statically analyse, which leads to for example bloated bundles. It’s also just not part of the ECMAScript specification, which ES modules are.

For anyone who is still wondering, ECMAScript (or ES) modules use a syntax with import thing from 'my-module'; or import { something } from 'my-module' to import things and export default or export something to expose entities from the module.

Bundlers like Webpack, Rollup and Parcel have support for ES modules. For a Node server I’ve still tended to write in CommonJS style because Node has great support for most ESNext features out of the box (eg. rest/spread, async/await, destructuring, class, shorthand object syntax) and I don’t like messing with bundlers and transpilers.

I’ve discovered the esm module, “Tomorrow’s ECMAScript modules today!” by John-David Dalton (of lodash 😄). It allows you to use ES modules in Node with no compilation step. It’s small, has a small footprint and comes with some extra goodies

What follows is some ways to use it that aren’t strictly documented. This covers use-cases like incremental adoption of ES modules (ie. convert some modules to ESM but not the whole app). Using this will help you share

Import default export from an ES module in CommonJS

const esmImport = require('esm')(module);
const foo = esmImport('./my-foo');
console.log(foo);

Import named exports from an ES module in CommonJS

const esmImport = require('esm')(module);
const { bar, baz } = esmImport('./my-foo');
console.log(bar, baz);

Re-export an ES module as CommonJS

This is documented in the docs but I thought I would include it for completeness

module.exports = require('esm')(module)('./my-es-module');
// see the docs
// https://github.com/standard-things/esm#getting-started

Load whole application using ES modules

Again, this in the docs but including it for completeness

node -r esm app.js
// see the docs
// https://github.com/standard-things/esm#getting-started

Using top-level await

Let’s say we have this module cli.module.js (taken from github.com/HugoDF/wait-for-pg):

const waitForPostgres = () => Promise.resolve();

try {
  await waitForPostgres();
  console.log('Success');
  process.exit(0);
} catch (error) {
  process.exit(1);
}

The interesting bit is that this is using await without being in an async function. That’s something esm allows you to do. This can be enabled by setting "esm": { "await": true } in package.json but it can also be enabled at conversion time cli.js:

const esmImport = require('esm')(module, { await: true });
module.exports = esmImport('./cli.module');

Lo and behold it works:

$ node cli.js
Success

That wraps up how to use ES modules now, without transpilation. There’s a more thorough walkthrough of what that means at ES6 by example: a module/CLI.

If you’re interested in “history of JavaScript module, bundling + dependency management” article, let me know by subscribing.

Top comments (0)