Overview

If I may say so myself, the struggle to understand the exports in CommonJS is all due to the poor way internet articles are written.

No offense, I always appreciate the help I get from online articles. But the way they describe the exports in CommonJS is appalling. I believe my explanation is the clearest in the world!

 

The so-called "two ways to export in CommonJS"

// Firstly, module.exports.
// Common explanation: The importer (require) can freely change the name with this.
module.exports = (i) => i + 1;

// Common explanation: Use this exports to avoid free name changes.
exports.increment = (i) => i + 1;

This phrase like "Use exports instead of module.exports". As exports is a shortcut for module.exports, it promotes the misunderstanding that they are different things even though they are the same.

 

Here's How CommonJS Exports are Understood by Mr. Green

  • In the CommonJS runtime environment, a set of "module scope variables" are automatically created for each file. They're similar to built-in variables in Python.
  • The module scope variables relevant to this matter are the following three:
    • module: This is an object that contains a property named exports (which is an empty object).
    • exports: This is a reference to module.exports ↑.
    • require: Basically, this is a function that refers to module.exports from a different file.

"In the CommonJS runtime environment, you can access the module.exports of another file using require()". In my understanding, this is the entirety of how exports and imports work in CommonJS. There are not two ways to do this.

 

The Following are Experiment Notes

I'll leave behind some notes on the experiments that led me to this understanding.

node -v
# v20.3.0

# This is like python -c in Python.
node --eval 'console.info({ module })'
# {
#   module: Module {
#     id: '[eval]',
#     path: '.',
#     exports: {},
#     filename: '/path/to/here/[eval]',
#     loaded: false,
#     children: [],
#     paths: [
#       ...
#       '/node_modules'
#     ]
#   }
# }

As you can see, in the Node.js (CommonJS) runtime environment, the module scope variable module is automatically defined. It's somewhat akin to __name__ in Python.

Now, what's making things complicated is the existence of the module scope variable exports. However, this guy is just a reference to module.exports. Let's take a look at it.

node --eval 'module.exports.foo = 1; exports.bar = 2; console.info({ exports })'
# { exports: { foo: 1, bar: 2 } }

See? Personally, I think it's better to always use module.exports and seal off exports.

And, require() simply returns the contents of module.exports from another file.

echo 'exports.increment = (i) => i + 1; module.exports.sunday = "Sunday";' > foo.js
node --eval 'console.info(require("./foo.js"))'
# { increment: [Function (anonymous)], sunday: 'Sunday' }

Therefore, the idea that there are two ways to export in CommonJS is a misconstrued notion. The default require function can access the module.exports property of the default module object in another file. That's all there is to it.