How to write CommonJS exports that can be name-imported from ESM

[2022-10-01] dev, javascript, nodejs
(Ad, please don’t block)

This blog post explores how to write CommonJS modules so that their exports can be name-imported from ESM modules on Node.js.

How to import CommonJS modules from ESM modules  

This is what the Node.js documentation says about this topic:

When importing CommonJS modules, the module.exports object is provided as the default export. Named exports may be available, provided by static analysis as a convenience for better ecosystem compatibility.

In other words:

  • Default-importing from CommonJS modules always works.
  • Name-importing only works sometimes.

How to write exports that can be name-imported  

Consider the following ESM module:

// main.mjs
import {namedExport} from './lib.cjs'; // (A)
console.log(namedExport); // 'yes'

The import in line A works if the CommonJS module looks like this:

// lib.cjs
exports.namedExport = 'yes';

We can also assign to module.exports.namedExport.

Exporting objects from CommonJS prevents named imports  

Assigning objects to module.exports prevents named imports:

// lib.cjs
module.exports = {
  namedExport: 'yes',
};

If we now run main.mjs, we get an error:

SyntaxError: Named export 'namedExport' not found.

Caveat: Named imports from CommonJS modules are not live connections  

In contrast to normal named imports, named CommonJS imports are not live connections:

// main.mjs
import {namedExport, changeNamedExport} from './lib.cjs';

console.log(namedExport); // 'yes'
changeNamedExport();
console.log(namedExport); // 'yes'
// lib.cjs
exports.namedExport = 'yes';
exports.changeNamedExport = () => {
  exports.namedExport = 'changed';
};

Further reading