Node.js now supports named imports from CommonJS modules, but what does that mean?
Tags: node, commonjs, ecmascript, modules
Published: 2020-10-02 10:32:36

---
title: Node.js now supports named imports from CommonJS modules, but what does that mean?
published: true
date: 2020-10-02 10:21:30 UTC
tags: node,CommonJS,ECMAScript,modules
canonical_url: https://simonplend.com/node-js-now-supports-named-imports-from-commonjs-modules-but-what-does-that-mean/
---

A couple of months ago I read the excellent blog post ‘[Node Modules at War: Why CommonJS and ES Modules Can’t Get Along](https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1)‘, and the reasons that CommonJS (CJS) and ECMAScript (ES) modules don’t play nicely together finally started to click for me.

When I saw this tweet the other day about the [release of v14.13.0 of Node.js](https://nodejs.org/en/blog/release/v14.13.0/), which introduces support for named exports from CommonJS modules, like many folk I was excited about CJS modules and ES modules working together better.

[![Tweet by @MylesBorins](https://simonplend.com/wp-content/uploads/2020/09/tweet-screenshot.png)](https://twitter.com/MylesBorins/status/1311033983824793601)

There is a huge existing ecosystem of packages for Node.js, many of which only expose a CJS module, not to mention a countless number of applications which are only using CJS modules. Anything which makes it easier to gradually migrate things to ES modules is good news in my book.

After the initial excitement about this release of Node.js subsided, I wondered to myself, "what does this new feature _actually_ mean?". To try and answer that question I installed Node.js v14.13.0 and started messing around with named exports and CJS modules – here’s what I learnt…

##### Jump Links

- [First Things First: What are named exports?](#first-things-first-what-are-named-exports)
- [The Big Gotcha: Named imports don’t work for all CJS modules](#the-big-gotcha-named-imports-dont-work-for-all-cjs-modules)
- [A Comedy of Errors: Trying out named imports with CJS modules](#a-comedy-of-errors-trying-out-named-imports-with-cjs-modules)
- [The Workaround: What to do when named imports don’t work for a CJS module](#the-workaround-what-to-do-when-named-imports-dont-work-for-a-cjs-module)
- [The Holy Grail: A CJS module that works with named imports](#the-holy-grail-a-cjs-module-that-works-with-named-imports)
- [Conclusion](#conclusion)

* * *

## First Things First: What are named exports?

Before we dive in to what named exports from CJS modules really means, let’s remind ourselves of what named exports are. ES modules define named exports like this:

```javascript
export function someFunction() {
	// Some great things would probably happen here
}

export const someObject = {
	// Some interesting object properties would be here
};

export const anotherFunction() {
	// Even greater things would probably happen here
}
```

And named imports which use them, look like this:

```javascript
import { someFunction, someObject } from "someModule";
```

This syntax allows you to only import specific named exports from an ES module – in the example code above we didn’t import `anotherFunction`.

## The Big Gotcha: Named imports don’t work for all CJS modules

If you take a look at [the pull request](https://github.com/nodejs/node/pull/35249#issuecomment-696942165) which introduced the named exports for CJS modules feature, you’ll see that a bunch of testing has been done which has shown that it will work for a decent amount of existing packages which expose CJS modules – enough to make it worth shipping this feature. The unstated implication here though is: named exports won’t work for all CJS modules in Node.js v14.13.0.

I learned this the hard way, so you don’t have to ![😅](https://s.w.org/images/core/emoji/13.0.0/72x72/1f605.png)

## A Comedy of Errors: Trying out named imports with CJS modules

The popular `lodash` package only exposes a CJS module, so it seemed like a good package to test named imports with:

```javascript
import { last } from "lodash";

const lastElement = last(["first", "second", "third"]);
console.log(lastElement);
```

When I ran this code with Node.js v14.13.0, I got this error:

```
$ node index.mjs

file:///home/simonplend/dev/personal/node-cjs-named-imports/index.mjs:51
import { last } from "lodash";
         ^^^^
SyntaxError: Named export 'last' not found. The requested module 'lodash' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'lodash';
const { last } = pkg;
```

Ok, no big deal. Next I tried using named imports with two other packages which only expose a CJS module, `winston` and `chalk`, but I received the same error. Huh.

Being the curious type, I read the [the pull request](https://github.com/nodejs/node/pull/35249) for the CJS named exports feature in more detail and noticed that it’s using a package named [cjs-module-lexer](https://github.com/nodejs/node/tree/master/deps/cjs-module-lexer). This package will "detect the most likely list of named exports of a CommonJS module". Cool. In the [Parsing Examples documentation](https://github.com/nodejs/node/blob/master/deps/cjs-module-lexer/README.md#parsing-examples) for this package it mentions that the matching rules it applies to find named exports will "underclassify in cases where the identifiers are renamed". I wondered if this was the reason that I was having problems using named imports.

I dug around in the `node_modules` directory for my test scripts and looked at the code for each of the packages which I’d tried to use named imports with. Boom! All of the CJS modules exposed by these packages rename the identifier for `exports` in some way. For `winston`, the renaming looks like this:

```javascript
/**
 * Uh oh, the identifier for `exports` has been renamed.
 *
 * This works because objects are assigned by reference in
 * JavaScript, however `cjs-module-lexer` won't be able to
 * detect any named exports that get attached to `winston`.
 */
const winston = exports;

winston.createLogger = require('./winston/create-logger');
```

I was three CJS packages in, and I still hadn’t found one that I could use with named imports. I did learn however, that even if you can’t use named imports with a CJS package, there is a workaround for this which should always work.

## The Workaround: What to do when named imports don’t work for a CJS module

Thankfully, when `cjs-module-lexer` has been unable to detect named exports for a CJS module and you try and use named imports with that module, the error message it gives you is pretty helpful (you’ll see this error in older versions of Node.js too):

```
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'lodash';
const { last } = pkg;
```

The good news is that, as the error message says, you can always import the default export from a CJS module in an ES module e.g. `import _ from 'lodash'`. The Node.js documentation explains why this works:

> The ECMAScript Module Namespace representation of a CommonJS module will always be a namespace with a `default` export key pointing to the CommonJS `module.exports` value.

_(Source: [Modules: ECMAScript modules – CommonJS Namespaces](https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_commonjs_namespaces))_

Once you’ve imported the default export from a CJS module you can then use [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) to unpack the named exports from the module object e.g. `const { last } = _;`

As this workaround only introduces one extra line of code and uses familiar syntax, it feels like a decent approach to me.

## The Holy Grail: A CJS module that works with named imports

Back to the emotional rollercoaster: I still hadn’t found a package with a CJS module that worked with named imports in Node.js v14.13.0. Then I gave it a try with `express`:

```javascript
import { Router } from "express";

const router = Router();
console.log({ router });
```

When I ran this I received… no errors! Only PURE SUCCESS! ![🎉](https://s.w.org/images/core/emoji/13.0.0/72x72/1f389.png)

```
$ node express.mjs

{
  router: [Function: router] {
    params: {},
    _params: [],
    caseSensitive: undefined,
    mergeParams: undefined,
    strict: undefined,
    stack: []
  }
}
```

I had proof at last, typed with my own fingers, in front of my own eyes, that named imports _can_ work for a CJS module in Node.js v14.13.0.

## Conclusion

In the Node.js documentation for ECMAScript Modules, under the section about ‘Interoperability with CommonJS’ which covers `import` statements, it mentions:

> Named exports may be available, provided by static analysis as a convenience for better ecosystem compatibility.

_(Source: [Modules: ECMAScript modules – Interoperability with CommonJS](https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_import_statements))_

This documentation is effectively stating that the feature of named imports for CJS modules is a _convenience_ and it cannot be relied upon. I guess I’d have been less surprised with the results in my testing if I’d seen this documentation beforehand.

I’m keen to figure out if there is a reliable automated way to determine if a CommonJS module is compatible with named imports in Node.js. If that’s possible then you could potentially point a script at a `package.json` file and have it tell you which CJS dependencies you can use named imports with. That would eliminate a lot of wasted time with trial and error when migrating a project codebase to use ES modules and `import` syntax.

It wasn’t as straightforward as I had hoped to use named imports with CommonJS modules, but I still think that support for this is a great addition to Node.js – it will certainly help ease the transition to ES modules, but don’t expect it to always "just work".