Working with Module Systems
1. Using CommonJS Modules
Example: CommonJS
// math.js
function add(a, b) { return a + b; }
module.exports = { add };
// index.js
const { add } = require("./math");
console.log(add(2, 3));
| Aspect | CommonJS |
| Loading | Synchronous |
| File ext | .js (default) or .cjs |
| Top-level await | Not supported |
| Globals available | require, module, exports, __dirname, __filename |
2. Understanding module.exports vs exports
Reference Model
module.exports ─────► { } ◄───── exports
▲
│ same object initially
│
exports.x = 1 → modifies that object → visible
exports = {...} → rebinds local var → IGNORED
3. Using Module Wrapper Function
Note: Each CJS module is wrapped: (function (exports, require, module, __filename, __dirname) { /* code */ }). That's why these "globals" exist.
| Wrapper Param | Purpose |
exports | Shortcut to module.exports |
require | Module loader |
module | Reference to current module |
__filename / __dirname | Path info |
4. Understanding Module Resolution
| Step | Lookup Order |
| 1 | Core module (fs, http, …) |
| 2 | Relative path (./, ../, /) with extensions .js, .json, .node, .mjs, .cjs |
| 3 | node_modules walking up to root |
| 4 | package.json "main" / "exports" field |
5. Using ES Modules
Example: ESM
// math.mjs
export function add(a, b) { return a + b; }
export default add;
// index.mjs
import add, { add as addNamed } from "./math.mjs";
const fs = await import("node:fs/promises"); // dynamic
console.log(add(2, 3));
| Aspect | ESM |
| Loading | Async, statically analyzable |
| File ext | .mjs or .js with "type":"module" |
| Top-level await | Supported v14.8+ |
| CJS interop | import x from "cjs-pkg" works |
6. Configuring Module Type
package.json | Effect |
"type": "module" | .js treated as ESM |
"type": "commonjs" (default) | .js treated as CJS |
| Force per file | Use .mjs (ESM) or .cjs (CJS) |
7. Using Dynamic Imports (import())
Example: Lazy load
async function loadHeavy() {
const { parse } = await import("./heavy-parser.js");
return parse(input);
}
| Property | Detail |
| Returns | Promise resolving to module namespace |
| Works in | Both CJS and ESM |
| Use cases | Conditional load, code splitting, ESM-from-CJS |
8. Using Import Assertions
Note: Replaced by import attributes using the with keyword v20.10+. Old assert syntax DEPRECATED.
Example: JSON import
import config from "./config.json" with { type: "json" };
9. Loading JSON Files
| System | Syntax |
| CJS | const cfg = require("./config.json"); |
| ESM | import cfg from "./config.json" with { type: "json" }; |
| Async | JSON.parse(await fs.readFile(p, "utf8")) |
10. Creating Conditional Exports
Example: package.json exports
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.cjs"
},
"./utils": "./dist/utils.js"
}
}
| Condition | Matches |
import | ESM consumer |
require | CJS consumer |
node / browser / deno | Runtime |
types | TypeScript |
default | Fallback |
11. Creating Dual Packages
| Strategy | Detail |
| Two builds | Output both .mjs and .cjs |
| Conditional exports | Map import/require |
| Avoid dual instance hazard | Don't store state in module scope |
12. Creating Module Aliases
| Approach | How |
| Subpath imports | "imports": { "#utils/*": "./src/utils/*.js" } in package.json |
tsconfig paths | For TypeScript projects (compile-time only) |
Node --import loader | Custom resolution hooks |