JavaScript (ES2026) Module 12 – Modules, packages and build tools
CommonJS, require(esm) and dual packages in Node
Read and write CommonJS, import it from ES modules, load ES modules with require() in Node.js 24, and publish one package for both without two copies.
What you will learn
- Read and write CommonJS modules with require, module.exports and exports
- Import CommonJS from an ES module, and predict which named exports Node.js finds
- Load ES modules with require() in Node.js 24, and explain when it throws
- Replace __dirname, __filename and require in ES modules with import.meta and createRequire
- Publish one package for both import and require without loading it twice
Before you start
What you’ll learn · Before you start
What you will learn
- Read and write CommonJS modules with require, module.exports and exports
- Import CommonJS from an ES module, and predict which named exports Node.js finds
- Load ES modules with require() in Node.js 24, and explain when it throws
- Replace __dirname, __filename and require in ES modules with import.meta and createRequire
- Publish one package for both import and require without loading it twice
Before you start
On this page
CommonJS is Node.js’s original module system. A file shares values by putting them on module.exports, and
another file loads it with require(), which reads, runs and returns it synchronously, on the spot.
ES modules came later and are the language’s standard, and Node.js 24 runs both
systems side by side: an ES module can import a CommonJS file, and require() can load an ES module, as a stable
feature since Node.js 24.15, provided no module of its graph uses top-level await. Plenty of existing code and many npm packages are CommonJS, so you will read it and
mix it with ES modules. This lesson shows how CommonJS works inside, what each direction of interop gives you, what
replaces __dirname in an ES module, and how a package with separate CommonJS and ES module builds gets loaded twice
in one program, with the fix.
How CommonJS works
Before Node.js runs a CommonJS file, it wraps the file’s code in a function with five parameters and calls it. The parameters are the module system:
// What Node.js gives every CommonJS file: the five parameters of the function it wraps the file in.
/* global require, module, exports, __filename, __dirname, arguments -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const { basename } = require('node:path');
console.log('parameters of the wrapper function:', arguments.length);
console.log('exports, require, module:', typeof exports, typeof require, typeof module);
console.log('__filename:', basename(__filename), '| __dirname:', basename(__dirname));
console.log('this is module.exports:', this === module.exports); Output
parameters of the wrapper function: 5 exports, require, module: object function object __filename: wrapper.cjs | __dirname: basics this is module.exports: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node wrapper.cjs
exports,requireandmoduleare parameters of that function, not globals, and so are__filenameand__dirname, the file’s own path and folder.- Variables declared at the top of the file are local to the function, so they stay private to the module.
thisat the top level ismodule.exports, whichexportsalso points to at the start.
require() finds a file, runs it once and keeps its module.exports in a cache keyed by the resolved file name:
basics/main.cjs
// Requires prices.cjs twice, then a module that assigns to exports instead of module.exports.
/* global require -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const first = require('./prices.cjs');
const second = require('./prices.cjs');
console.log('₹100 with GST:', first.withGst(10000) / 100);
console.log('the same object both times:', first === second);
console.log('kept in require.cache:', Object.keys(require.cache).some((file) => file.endsWith('prices.cjs')));
console.log('wrong-exports.cjs gives:', JSON.stringify(require('./wrong-exports.cjs'))); Output
prices.cjs runs
₹100 with GST: 118
the same object both times: true
kept in require.cache: true
wrong-exports.cjs gives: {}
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.cjs
basics/prices.cjs
// A CommonJS module: whatever ends up in module.exports is what require() returns.
/* global require, module -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
console.log('prices.cjs runs');
// No extension: require() tries .js, .json and .node in turn, and finds rates.json.
const rates = require('./rates');
function withGst(paise) {
return paise + Math.round(paise * rates.gst);
}
module.exports = { withGst }; basics/rates.json
{ "gst": 0.18 } basics/wrong-exports.cjs
// Assigning to `exports` only rebinds a local variable: require() returns module.exports, which is still {}.
/* eslint-disable @typescript-eslint/no-unused-vars -- the assignment below is the mistake this file shows */
/* global exports: writable -- CommonJS passes these to each file (for the linter) */
exports = { gst: 0.18 }; prices.cjs runs appears once: the second require() returned the same object from require.cache. Inside
prices.cjs, require('./rates') has no extension: CommonJS tries .js, .json and .node in turn, and here it
found rates.json, which it parsed as JSON. wrong-exports.cjs shows the classic mistake: exports = { … } only points
the local parameter at a new object, while require() returns module.exports, still the empty object. Add
properties with exports.gst = 0.18, or replace the whole value with module.exports = ….
Cycles in CommonJS
When two files require each other, the second one gets the first one’s exports as they are at that moment:
cycle/main.cjs
// Starts the cycle by requiring inventory.cjs, then uses both modules once they have finished.
/* global require -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const inventory = require('./inventory.cjs');
console.log(inventory.reserve('pen'));
console.log(inventory.reserve('notebook')); Output
inventory.cjs starts orders.cjs starts orders.cjs sees these exports of inventory.cjs: stock inventory.cjs ends pen: reserved notebook: out of stock
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.cjs
cycle/inventory.cjs
// inventory.cjs and orders.cjs require each other.
/* global require, exports -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
console.log('inventory.cjs starts');
exports.stock = { pen: 3, notebook: 0 };
const orders = require('./orders.cjs');
exports.reserve = (item) => orders.place(item);
console.log('inventory.cjs ends'); cycle/orders.cjs
// Required by inventory.cjs while inventory.cjs is still running.
/* global require, exports -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
console.log('orders.cjs starts');
const inventory = require('./inventory.cjs');
console.log('orders.cjs sees these exports of inventory.cjs:', Object.keys(inventory).join(', '));
exports.place = (item) => `${item}: ${inventory.stock[item] > 0 ? 'reserved' : 'out of stock'}`; inventory.cjs was still running when orders.cjs asked for it, so the cache handed back its unfinished exports,
with stock but no reserve. Nothing ran twice and nothing looped. The later calls still worked, because
orders.cjs kept a reference to the same exports object, which inventory.cjs went on filling. Had
inventory.cjs ended with module.exports = { stock, reserve } instead, orders.cjs would have kept the old, empty
object for good. ES modules handle cycles with live bindings instead, so a let, const or class read before its
module has run is a ReferenceError there rather than a silent undefined.
| Criterion | CommonJS | ES modules |
|---|---|---|
| Loading | require(), synchronous, wherever it is called | import, connected before any code runs; import() at run time |
| What importers get | The value module.exports has at that moment | Live, read-only bindings |
| Extension in relative paths | Optional: .js, .json and .node are tried | Required |
| JSON files | require('./rates.json') | import with { type: 'json' } |
| Top-level await | A SyntaxError | Allowed |
| Own path and folder | __filename, __dirname | import.meta.filename, import.meta.dirname |
| Strict mode | Only with 'use strict' | Always |
Node.js decides per file which system runs it: .cjs is CommonJS and .mjs an ES module, while a .js file
follows the "type" field of the nearest package.json; the lesson on running
JavaScript lists every case. The examples here use .cjs and .mjs, so they
mean the same wherever they are saved. A package.json with a syntax error, such as a trailing comma, stops even a
plain node app.js with ERR_INVALID_PACKAGE_CONFIG, because Node.js reads it to decide the file’s type.
How the two module systems of Node.js load each other
Text description of the diagram
The diagram is a small tree of boxes joined by arrows, read from the top.
- An ES module imports a CommonJS module with `import`. The default export is the CommonJS file's `module.exports`, and named imports work for the names Node.js can find by reading the file.
- The CommonJS module can load an ES module whose graph has no top-level await with `require()`, which returns the module's namespace object. With top-level await anywhere in the graph, `require()` throws `ERR_REQUIRE_ASYNC_MODULE`.
- The CommonJS module can load any ES module with `import()`, which returns a promise for the namespace.
Importing CommonJS from an ES module
An ES module can import a CommonJS file directly. The default import is always the file’s module.exports:
esm-imports-cjs/main.mjs
// An ES module importing two CommonJS files.
import rates, { gstRate } from './rates.cjs';
import * as namespace from './rates.cjs';
import built from './built.cjs';
console.log('named import from rates.cjs:', gstRate);
console.log('default import is module.exports:', JSON.stringify(rates));
console.log('names on the namespace:', Object.keys(namespace).join(', '));
console.log('built.cjs through its default import:', built.gstRate); Output
named import from rates.cjs: 0.18
default import is module.exports: {"gstRate":0.18,"currency":"INR"}
names on the namespace: currency, default, gstRate, module.exports
built.cjs through its default import: 0.18
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.mjs
esm-imports-cjs/rates.cjs
// Exports added one property at a time: a pattern Node.js can read without running the file.
/* global exports -- CommonJS passes these to each file (for the linter) */
exports.gstRate = 0.18;
exports.currency = 'INR'; esm-imports-cjs/built.cjs
// module.exports set to whatever a function returns: its names are known only once the file has run.
/* global module -- CommonJS passes these to each file (for the linter) */
function build() {
return { gstRate: 0.18, currency: 'INR' };
}
module.exports = build(); Named imports are a convenience on top. An ES module’s imports are connected before any code runs, so Node.js
cannot run the CommonJS file to see what it exports; it reads the source instead and recognises common patterns, such
as exports.gstRate = …. That worked for rates.cjs, whose namespace offers gstRate and currency, the default,
and the same value once more under the name 'module.exports'. These named exports are copies made once, not live
bindings. built.cjs sets module.exports to whatever a function returns, which no reading of the source can see,
so a named import from it fails before anything runs:
interop-errors.mjs
// Two mistakes when ES modules meet CommonJS, run with Node.js.
import { session } from './session.mjs';
session(['node esm-imports-cjs/named-from-built.mjs', 'node paths/uses-dirname.mjs']); Output
$ node esm-imports-cjs/named-from-built.mjs SyntaxError: Named export 'gstRate' not found. The requested module './built.cjs' is a CommonJS module, which may not support all module.exports as named exports. [exit status 1] $ node paths/uses-dirname.mjs ReferenceError: __dirname is not defined in ES module scope [exit status 1]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node interop-errors.mjs
esm-imports-cjs/named-from-built.mjs
// A named import from built.cjs, whose exports static analysis cannot see.
import { gstRate } from './built.cjs';
console.log(gstRate); paths/uses-dirname.mjs
// CommonJS habits in an ES module.
// eslint-disable-next-line no-undef -- the mistake this file shows
console.log(__dirname); The full message goes on to suggest the fix: import the default and take the property from it, as main.mjs did
with built.gstRate. When a named import from a CommonJS package fails, that is the answer that always works.
require() of ES modules in Node.js 24
The other direction used to be impossible: require() of an ES module threw ERR_REQUIRE_ESM. Node.js added it
behind a flag in 22.0 and 20.17, turned it on by default in 23.0, 22.12 and 20.19, and declared it stable in 24.15.
process.features.require_module tells a program whether it is available.
require-esm/main.cjs
// CommonJS code loading three ES modules with require() in Node.js 24.
/* global require -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const money = require('./money.mjs');
console.log('require(money.mjs) gives a namespace with:', Object.keys(money).join(', '));
console.log('new money.default(500).paise:', new money.default(500).paise);
const price = require('./price.mjs');
console.log("price.mjs exports 'module.exports', so require() gives:", typeof price, price(4999));
try {
require('./slow.mjs');
} catch (error) {
console.log('require(slow.mjs):', error.code);
}
console.log('process.features.require_module:', process.features.require_module); Output
require(money.mjs) gives a namespace with: __esModule, currency, default new money.default(500).paise: 500 price.mjs exports 'module.exports', so require() gives: function ₹49.99 require(slow.mjs): ERR_REQUIRE_ASYNC_MODULE process.features.require_module: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.cjs
require-esm/money.mjs
// An ES module with a default export and a named export.
export default class Money {
constructor(paise) {
this.paise = paise;
}
}
export const currency = 'INR'; require-esm/price.mjs
// An ES module that tells require() what to return: the export named 'module.exports'.
export default function price(paise) {
return `₹${(paise / 100).toFixed(2)}`;
}
export { price as 'module.exports' }; require-esm/slow.mjs
// An ES module with top-level await.
await Promise.resolve();
export const ready = true; require()returns the module’s namespace object, so a default export is.default. Node.js adds__esModule: truewhen the module has a default export, a marker that code compiled from ES modules to CommonJS by build tools looks for; code you write by hand should not depend on it.- An ES module can choose what
require()returns by exporting a value under the string name'module.exports', asprice.mjsdoes. CommonJS callers then get the function itself, but they lose the module’s other named exports unless those are attached to that value. slow.mjsuses top-level await, andrequire()refused it withERR_REQUIRE_ASYNC_MODULE: a synchronous call cannot wait for a module graph that waits. The same error comes from top-level await anywhere in the graph, not only in the file you require. CommonJS code can still useimport()for such modules.
__dirname, __filename and require() in ES modules
ES modules have no wrapper function, so the five CommonJS variables do not exist in them. The session above showed
the error you get, ReferenceError: __dirname is not defined in ES module scope. Here is what replaces them:
paths/paths.mjs
// What replaces __dirname, __filename and require() in an ES module.
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { basename, join } from 'node:path';
console.log('import.meta.filename ends with:', basename(import.meta.filename));
console.log('import.meta.dirname ends with:', basename(import.meta.dirname));
const greeting = readFileSync(join(import.meta.dirname, 'greeting.txt'), 'utf8').trim();
console.log('read next to the module:', greeting);
console.log('the same through a URL:', readFileSync(new URL('./greeting.txt', import.meta.url), 'utf8').trim() === greeting);
const require = createRequire(import.meta.url);
console.log('createRequire() loads JSON synchronously:', require('./settings.json').currency);
console.log('typeof __dirname here:', typeof __dirname); Output
import.meta.filename ends with: paths.mjs import.meta.dirname ends with: paths read next to the module: Namaste from a file next to the module the same through a URL: true createRequire() loads JSON synchronously: INR typeof __dirname here: undefined
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node paths.mjs
paths/greeting.txt
Namaste from a file next to the module paths/settings.json
{ "currency": "INR" } import.meta.filename and import.meta.dirname are the module’s own path and folder (Node.js 20.11 and later), and
import.meta.url is its file: URL, which new URL('./greeting.txt', import.meta.url) turns into the address of a
file next to it. Both point at the module’s folder, not the folder the program was started from, so a script finds
its files wherever someone runs it. When an ES module needs a synchronous require(), for example for a package that
is CommonJS only, createRequire(import.meta.url) makes one that resolves relative to the module.
One package for import and require
A package’s "exports" field in package.json lists its public entry points. Anything it does not list cannot be
imported by its package name, which keeps internal files internal. An entry can depend on conditions, which
Node.js checks in the order they are written: "import" matches import and import(), "require" matches
require(), "module-sync" matches both for an ES module without top-level await, "node" matches any Node.js, and
"default" matches everything and goes last. The "imports" field adds private aliases that start with #, usable
only inside the package.
The tempting design is two builds, one ES module behind "import" and one CommonJS file behind "require". Here
both versions of a package called paise are used by an app that has an ES module and a CommonJS file, each using
the package by its own name, which a package with "exports" may do:
hazard/package.json · dual/hazard/package.json
{
"name": "paise",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./money.mjs",
"require": "./money.cjs"
}
}
} hazard/money.mjs · dual/hazard/money.mjs
// The ES module build of the package.
console.log('money.mjs runs');
export class Money {
constructor(paise) {
this.paise = paise;
}
} hazard/money.cjs · dual/hazard/money.cjs
// The CommonJS build of the same package: the same class, written again.
/* global module -- CommonJS passes these to each file (for the linter) */
console.log('money.cjs runs');
class Money {
constructor(paise) {
this.paise = paise;
}
}
module.exports = { Money }; hazard/legacy.cjs · dual/hazard/legacy.cjs
// CommonJS code that uses the package by its name; require() takes the "require" condition of "exports".
/* global require, module -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const { Money } = require('paise');
module.exports = { price: new Money(4999) }; hazard/app.mjs · dual/hazard/app.mjs
// An ES module that uses the package by its name (a package may import itself), and the CommonJS file too.
import { Money } from 'paise';
import legacy from './legacy.cjs';
console.log('legacy.price instanceof Money:', legacy.price instanceof Money); fixed/package.json · dual/fixed/package.json
{
"name": "paise",
"version": "2.0.0",
"type": "module",
"exports": {
".": "./money.mjs"
},
"imports": {
"#format": "./format.mjs"
}
} fixed/money.mjs · dual/fixed/money.mjs
// The only build of the package, an ES module. "#format" is a private alias from the "imports" field.
import { rupees } from '#format';
console.log('money.mjs runs');
export class Money {
constructor(paise) {
this.paise = paise;
}
toString() {
return rupees(this.paise);
}
} fixed/peek.mjs · dual/fixed/peek.mjs
// Tries to import a file that the package's "exports" does not list.
import { rupees } from 'paise/format.mjs';
console.log(rupees(100)); // Both versions of the package, run with Node.js, and an import of a file the package does not export.
import { session } from '../session.mjs';
session(['node hazard/app.mjs', 'node fixed/app.mjs', 'node fixed/peek.mjs']); Output
$ node hazard/app.mjs money.mjs runs money.cjs runs legacy.price instanceof Money: false $ node fixed/app.mjs money.mjs runs legacy.price instanceof Money: true legacy.price is ₹49.99 $ node fixed/peek.mjs Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './format.mjs' is not defined by "exports" in ./fixed/package.json imported from ./fixed/peek.mjs [exit status 1]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs
With two builds, both files ran, so the program held two unrelated Money classes, and a price made by the CommonJS
side was not an instanceof the class the ES module side imported. The same split doubles anything a package keeps
in module state: a cache, a registry of plug-ins or a configuration set by one side is invisible to the other. This
is the dual package hazard. In the fixed version, "exports" sends everyone to one ES module, require() loads
that same module, and the check is true. fixed/peek.mjs also shows the encapsulation: paise/format.mjs is not in
"exports", so importing it by the package name fails with ERR_PACKAGE_PATH_NOT_EXPORTED, while the package itself
reaches it through its private #format alias.
So for a new package, publish ES modules only and let require() load them, which Node.js 20.19, 22.12 and every
later release can do. Keep top-level await out of the package’s entry points, or CommonJS users get
ERR_REQUIRE_ASYNC_MODULE. If you must keep a CommonJS build for older Node.js versions, keep the real code, and any
state, in one of the two builds and make the other a thin wrapper around it, so that only one copy can exist.
Exercise: a tiny require() with a cache
makeRequire() should run each module’s function once, keep its exports in a cache, hand a cycle the exports filled
in so far, throw MODULE_NOT_FOUND for unknown names, and forget a module whose function threw. The sample tests
check each of those.
Exercise · Medium · JavaScript
Build a tiny CommonJS require() with a module cache
Node.js runs a CommonJS file as a function, (exports, require, module, __filename, __dirname) => { … }, and keeps every module it has loaded in a cache. Rebuild the core of that. makeRequire(modules) takes an object that maps module names to such functions, here with three parameters, (exports, require, module), and returns a require(name) that:
- the first time a name is required, makes
module = { exports: {} }, calls the module's function withmodule.exports, the samerequireandmodule, and returnsmodule.exports, soexports.rate = 0.18andmodule.exports = fnboth work; - returns the cached
module.exportson later calls, without calling the function again; - returns the exports filled in so far when a module requires another module that is still running, so two modules that require each other finish instead of looping for ever;
- throws an
Errorwhosecodeis'MODULE_NOT_FOUND'for a name it does not know; - forgets a module whose function threw, so that requiring it again runs it again, as Node.js does.
Each require made by makeRequire() has its own cache. Run the sample tests when you are done.
Starter code · mini-require.mjs
// mini-require.mjs: the core of CommonJS's require() (the exercise of lesson javascript/commonjs-and-interop).
export function makeRequire(modules) {
function require(name) {
const module = { exports: {} };
modules[name](module.exports, require, module);
return module.exports;
}
return require;
} The sample tests · mini-require.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { makeRequire } from './mini-require.mjs';
test('returns module.exports, filled through exports or replaced', () => {
const require = makeRequire({
rates: (exports) => {
exports.gst = 0.18;
},
add: (exports, require, module) => {
module.exports = (a, b) => a + b;
},
});
assert.deepEqual(require('rates'), { gst: 0.18 });
assert.equal(require('add')(2, 3), 5);
});
test('runs each module once and returns the cached exports', () => {
let runs = 0;
const require = makeRequire({
counter: (exports) => {
runs += 1;
exports.count = 0;
},
});
const first = require('counter');
const second = require('counter');
assert.equal(first, second);
assert.equal(runs, 1);
});
test('a cycle gets the exports filled in so far', () => {
// Without a cache the two modules would require each other for ever; stop after a few runs instead.
let runs = 0;
const guard = () => {
runs += 1;
if (runs > 4) throw new Error('the two modules keep running each other: is a module cached before its function runs?');
};
const require = makeRequire({
inventory: (exports, require) => {
guard();
exports.stock = 3;
const orders = require('orders');
exports.sawOrders = typeof orders.place;
},
orders: (exports, require) => {
guard();
const inventory = require('inventory');
exports.place = () => 'placed';
exports.sawStock = inventory.stock;
exports.sawSawOrders = inventory.sawOrders;
},
});
const inventory = require('inventory');
assert.equal(inventory.sawOrders, 'function');
assert.equal(require('orders').sawStock, 3);
assert.equal(require('orders').sawSawOrders, undefined);
assert.equal(runs, 2);
});
test('an unknown name throws an Error with code MODULE_NOT_FOUND', () => {
const require = makeRequire({});
try {
require('missing');
assert.ok(false, 'require("missing") should throw');
} catch (error) {
assert.equal(error.code, 'MODULE_NOT_FOUND');
assert.ok(error instanceof Error);
}
});
test('a module that threw is run again on the next require', () => {
let runs = 0;
const require = makeRequire({
flaky: (exports) => {
runs += 1;
if (runs === 1) throw new Error('first run fails');
exports.ok = true;
},
});
assert.throws(() => require('flaky'), Error);
assert.deepEqual(require('flaky'), { ok: true });
assert.equal(runs, 2);
});
test('each require has its own cache', () => {
let runs = 0;
const modules = { once: () => (runs += 1) };
makeRequire(modules)('once');
makeRequire(modules)('once');
assert.equal(runs, 2);
}); A hint
Keep a Map from name to module object inside makeRequire(), and look a name up there before anything else. Put the new module into the map before you call its function: that is what lets a cycle find the half-finished exports instead of running the modules again and again. Wrap the call in try, and in catch delete the name from the map and throw the error again.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between module.exports and exports in CommonJS?
module.exports is the value that require() returns for a file. exports is a parameter of the wrapper function
that starts out pointing at the same object, as a shortcut. Adding properties through either one is the same thing:
exports.rate = 0.18 puts rate on module.exports. Assigning to them is not: module.exports = fn replaces what
the file exports, while exports = fn only points the local parameter at something else, and require() still
returns the original, empty object. So use exports.name = … to add exports, use module.exports = … to export one
function or class, and never assign to exports itself. After module.exports has been replaced, exports no
longer points at it either.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your library ships an ES module build and a CommonJS build behind the "import" and "require" conditions. A user reports that error instanceof LibraryError is false for errors thrown by their CommonJS plug-in, and that plug-ins registered in one part of their app are missing in another. Explain, prove it, and redesign the package.
-
Fresher to mid
After moving a service to ES modules, import { parse } from 'legacy-config' fails at start-up with "Named export 'parse' not found", although require('legacy-config').parse works. The package is CommonJS and ends with module.exports = createApi(). Explain why, give two fixes, and say which you would choose.
Key takeaways
- A CommonJS file runs inside a function whose parameters are
exports,require,module,__filenameand__dirname;require()runs a file once and caches itsmodule.exports. - In a cycle, CommonJS hands back the exports filled in so far; replacing
module.exportsat the end leaves the other module with the old object. - An ES module imports CommonJS as a default export,
module.exports, plus the named exports Node.js can find by reading the source; fall back to the default import when a name is not found. - In Node.js 24,
require()loads ES modules and returns their namespace, unless top-level await appears anywhere in the graph (ERR_REQUIRE_ASYNC_MODULE). - In ES modules, use
import.meta.dirname,import.meta.filename,import.meta.urlandcreateRequire(). - Separate CommonJS and ES module builds behind
"require"and"import"load a package twice; publish one ES module build, or make one build a wrapper around the other.
References
- Node.js 24 documentation: Modules: CommonJS modules (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Loading ECMAScript modules using require() (OpenJS Foundation (Node.js))
- Node.js 24 documentation: The module wrapper (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Cycles (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Interoperability with CommonJS (OpenJS Foundation (Node.js))
- Node.js 24 documentation: import.meta.dirname (OpenJS Foundation (Node.js))
- Node.js 24 documentation: module.createRequire() (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Package entry points (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Conditional exports (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Subpath imports (OpenJS Foundation (Node.js))
- Node.js 24 documentation: Self-referencing a package using its name (OpenJS Foundation (Node.js))
- Node.js package examples (OpenJS Foundation (Node.js))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress