Handbooks / JavaScript / Chapter 8
Modules and Tooling
7 pages · ~5 min✓ Reviewed
Builds on Errors and Debugging. Next up: DOM and Events.
Modules: The Mental Model
1. A Module Is a File
- Own scope: top-level variables are private unless exported
- Evaluated once, then cached
- CJS:
require()+module.exports, synchronous, runtime; default.jsin Node - ESM: static
import/export, parsed before any code runs; browsers + Node
2. CJS vs ESM
| CJS | ESM | |
|---|---|---|
| Loading | dynamic, synchronous | static, async-capable |
| Exports | value copies | live bindings |
| Mode | sloppy by default | strict always |
| Globals | require, __dirname, __filename | none of these |
- Parsefind imports
- Linkwire bindings
- Evaluaterun once
3. Interview Point
- Tree-shake ESM but not CJS? ESM imports are static, so unused exports are known before running
- Static analysis? Reading imports and exports from syntax alone, without executing code
- Evaluated once? The cache is keyed by resolved path, so side effects and state are shared
Remember
require is a runtime call, so tools cannot know what it loads. import is syntax, so they can.
Import and Export Forms
1. Named, default, namespace
- Default: one per module, imported under any name
- Namespace object is read-only
| Form | Export | Import |
|---|---|---|
| Named | export const a = 1 | import { a, f } from './m.js' |
| Default | export default expr | import Foo from './m.js' |
| Namespace | none | import * as ns from './m.js' |
Live Bindings and Dynamic Import
1. Live vs Snapshot
- Live binding: import = read-only view of exporter's variable
- Importer assigning to it → TypeError
| ESM | CJS | |
|---|---|---|
| Exports | live view | value copy |
| Later change | visible | stale |
2. import() and Top-Level await
import()returns a Promise of the module namespace
Node Interop and package.json type
1. How Node Picks the Type
| File | Treated as |
|---|---|
.mjs | ESM always |
.cjs | CJS always |
.js + "type": "module" | ESM |
.js + "commonjs" or none | CJS (default) |
- Nearest package.json decides
.js - Node 22.7+/20.19+ sniff ESM syntax when no type; set
typeanyway
Bundlers, Tree Shaking and npm Scripts
1. What a Bundler Does
- Entrystart file
- Import graphresolve all
- Tree shakedrop unused
- Bundlesoptimised
- Tools: webpack, Rollup, Vite/esbuild
- Each dynamic
import()becomes its own chunk (code splitting)
2. Tree Shaking and sideEffects
- Tree shaking drops unused exports
- Needs static ESM syntax
- CJS and dynamic
requiredefeat it "sideEffects": false= unused modules are safe to remove- An array lists files to keep: CSS, polyfills
ESM and CJS Cheat Sheet
1. Syntax side by side
| CJS | ESM | |
|---|---|---|
| Import | const x = require('x') | import x from 'x' |
| Named | const { a } = require('x') | import { a } from 'x' |
| Export value | module.exports = v | export default v |
| Export many | module.exports = { a, b } | export { a, b } |
| Export one | exports.a = 1 |
Check yourself
m.js has export let count = 0; export function inc(){ count++ }. After import { count, inc } from './m.js'; inc(); what does console.log(count) print? And with const { count } = require('./m') in CJS?
- ESM prints
1: the import is a live, read-only view of the exporter's variable. - CJS prints
0: destructuring copied the value at require time.
import { count, inc } from './m.js'; inc(); console.log(count);
package.json has "type": "module". app.js runs const fs = require('fs') and crashes. Why, and what are two fixes?
.jsis now ESM, whererequireis not defined.- Use
import fs from 'fs', or rename the file toapp.cjs.