#CommonJS: require and module.exports
CommonJS is the module system Node shipped with: a file exports by putting something on module.exports, and another file gets it with require(). The JavaScript site's CommonJS topic covers the syntax and the exports vs module.exports trap. This topic is about what Node is actually doing when you call require.
The key idea: require is an ordinary, synchronous function that runs another file and hands you whatever that file left in module.exports. Nothing is analysed ahead of time. When execution reaches a require call, Node works out which file it means, runs that file from top to bottom (unless it already ran), and returns. The mental model is a function call that happens to execute a whole file as its body.
To make that possible, Node never runs your CommonJS file as-is. It wraps the source in a function — (function (exports, require, module, __filename, __dirname) { … }) — and calls it. Everything that looks like a global in a CommonJS file is really one of those five parameters, which you can prove:
console.log(arguments.length); // the wrapper's five parameters
console.log(typeof require, typeof module, typeof exports);
console.log(this === module.exports, exports === module.exports);
console.log(__filename.split('/').pop(), __dirname.split('/').pop());
console.log('loaded while running?', module.loaded);
console.log('am I the entry point?', require.main === module);
setImmediate(() => console.log('loaded afterwards?', module.loaded));5
function object object
true true
wrapper-demo.cjs cjs1
loaded while running? false
am I the entry point? true
loaded afterwards? truearguments.lengthis5. Top-level code in a normal script has noargumentsat all — it only exists here because your file is the body of a function.require('node:module').wrappershows the exact wrapper text:'(function (exports, require, module, __filename, __dirname) { 'and'\n});'.require,moduleandexportsare the wrapper's parameters, sotypeofsees a function and two objects. The same wrapping is why a top-levelconstin one file never leaks into another — it's a local variable of that file's function.- Node calls the wrapper with
thisset tomodule.exports, so top-levelthis === module.exportsistrue.exportsstarts as another reference to that same object, which is whyexports.add = …works andexports = …doesn't. __filenameand__dirnameare the absolute path of this file and its folder (trimmed here to the last segment; the demo ran in a folder calledcjs1).module.loadedisfalsewhile the file body is running andtrueonce it has finished. That flag is what a circularrequirecan observe — see Circular dependencies.require.main === moduleistrueonly for the file you started withnode file.cjs. It's the CommonJS idiom for "run this only when executed directly, not when required" (Python'sif __name__ == "__main__").
math.cjsNow watch the timing. Each require runs its file at the moment execution reaches it, and the caller waits:
console.log(' math.cjs: running');
let calls = 0;
function add(a, b) {
calls++;
return a + b;
}
module.exports = { add, getCalls: () => calls };
console.log(' math.cjs: done');app.cjs: static and conditional requireconsole.log('app: before require');
const math = require('./math.cjs');
console.log('app: after require, got', Object.keys(math));
console.log('app: add(2, 3) =', math.add(2, 3), '| calls =', math.getCalls());
if (process.argv.includes('--report')) {
const path = require('node:path'); // conditional, runtime require
console.log('app: report dir', path.basename(__dirname));
}require runs$ node app.cjs --report
app: before require
math.cjs: running
math.cjs: done
app: after require, got [ 'add', 'getCalls' ]
app: add(2, 3) = 5 | calls = 1
app: report dir cjs1app.cjsstarts and printsapp: before require. Nothing frommath.cjshas run yet — CommonJS doesn't look ahead.require('./math.cjs')resolves the path, finds no cached copy, creates amoduleobject, and callsmath.cjs's wrapper.app.cjsis paused inside that call, exactly as it would be inside any function call.math.cjsprints both of its lines, creates the privatecallsvariable, and assignsmodule.exports = { add, getCalls }. When the wrapper returns,requirereturns that object.- Back in
app.cjs,math.add(2, 3)returns5and bumpscallsfrom0to1.callsisn't exported, butgetCallscloses over it — module scope gives you privacy for free. - The
require('node:path')inside theifonly runs when you pass--report. Becauserequireis just a function, it can sit in a branch, a loop or a function body, and take a computed string. That flexibility is also the downside: tools can't know a file's dependencies without running it.
exports