# Modules (ES Modules)

## 1. Why Modules Exist

Before ES modules, all scripts shared one global scope — every `<script>` tag's top-level variables collided in the same namespace. Modules give each file its **own scope**, with explicit, opt-in sharing via `import`/`export`.

## 2. Enabling ES Modules

| Context | How to enable |
|---|---|
| Browser | `<script type="module" src="app.js"></script>` |
| Node.js | file extension `.mjs`, OR `"type": "module"` in `package.json` for `.js` files |

## 3. Named Exports

```js
// math.js
export const PI = 3.14159;
export function square(x) { return x * x; }
export class Vector { /* ... */ }

// or export a group at the bottom:
const E = 2.71828;
function cube(x) { return x ** 3; }
export { E, cube };
```

```js
// app.js
import { PI, square, cube as cubeFn } from "./math.js"; // rename on import with `as`
```

Named exports can be imported selectively, and renamed on either side.

## 4. Default Exports

```js
// user.js
export default class User { /* ... */ }
```

```js
// app.js
import User from "./user.js"; // no curly braces, and the local name can be ANYTHING
import AnyNameAtAll from "./user.js"; // still works — default import isn't tied to the export's name
```

A module can have **at most one** default export, but any number of named exports — often combined.

## 5. Namespace Import

```js
import * as math from "./math.js";
math.square(4); // access everything as properties of the namespace object
```

## 6. Re-exporting (Barrel Files)

```js
// index.js — collects and re-exports from multiple files
export { square, cube } from "./math.js";
export { default as User } from "./user.js";
export * from "./constants.js";
```

Common for creating a single clean entry point (`import { square, User } from "./index.js"`) instead of many deep import paths.

## 7. Module Scope and Hoisting Behavior

```js
console.log(later); // ReferenceError — imports are live bindings, but hoisted like let/const
import { later } from "./somewhere.js";
```

`import` statements are hoisted to the top of the module (they must run before any other code can use them), but the actual binding follows the same TDZ-like rules as `let`/`const` for anything computed at module-evaluation time.

## 8. Live Bindings, Not Copies

```js
// counter.js
export let count = 0;
export function increment() { count++; }
```

```js
// app.js
import { count, increment } from "./counter.js";
console.log(count); // 0
increment();
console.log(count); // 1 — the imported binding reflects changes in the source module
```

Unlike CommonJS's `module.exports` (which exports a snapshot object), ES module imports are **live references** to the exporting module's bindings.

![app.js's imported count is an indirect link into counter.js's real binding slot, not a copy](diagrams/01-live-binding.svg)

## 9. CommonJS vs ES Modules (Quick Contrast)

| | CommonJS (`require`) | ES Modules (`import`) |
|---|---|---|
| Syntax | `const x = require("./x")` | `import x from "./x.js"` |
| Loading | synchronous, dynamic (any code path) | static (top-level only, analyzed before execution) — plus dynamic `import()` |
| Exports | `module.exports = {...}` (snapshot) | live bindings |
| File extension needed in import path | usually optional | usually required (`./x.js`, not `./x`) |
| Default in Node | `.js` files (unless `"type": "module"`) | `.mjs`, or `"type": "module"` |

(Full comparison covered in the Node.js CommonJS vs ESM topic.)

## Under the Hood: Linking Before Execution, and What "Live Binding" Really Means

ES modules are loaded in **two distinct, separate phases** — this two-phase design is precisely why static `import`/`export` behaves so differently from a CommonJS `require()` call.

### Phase 1 — Instantiation (linking), no code runs yet

The engine parses every module in the dependency graph, builds a **module record** for each one listing its imports and exports, and connects — "links" — each `import` to the exact binding slot it refers to in the source module. Critically, **no module body code executes during this phase** — it's pure static analysis of the import/export graph.

```
app.js imports { count } from counter.js
   ↓ linking phase resolves this to: app.js's `count` binding IS counter.js's `count` slot,
     not a copy of whatever value it currently holds (it may not even be initialized yet)
```

This static, execute-nothing-yet phase is *why* `import` paths can't be computed dynamically at the top level (`import { x } from someVariable` is a syntax error) — the linker needs to know every dependency before anything runs, which is also what enables tools to catch a typo'd import name before your program ever executes a single line.

### Phase 2 — Evaluation, exactly once per module, in dependency order

Only after linking completes does the engine actually run each module's top-level code, exactly once, visiting dependencies before dependents (a module is evaluated before anything that imports from it, following the dependency graph — cycles are handled by evaluating in the order first encountered and leaving not-yet-reached bindings in their TDZ-like unset state, which is the deeper mechanism behind the "partially initialized module" pitfall).

### Live bindings are literally indirect references, not copied values

```js
// counter.js
export let count = 0;
export function increment() { count++; }
```

When `app.js` does `import { count } from "./counter.js"`, the linking phase does **not** copy `0` into a new local variable called `count`. It creates an **indirect binding** — `app.js`'s `count` is wired directly to `counter.js`'s actual `count` slot (the same kind of binding-slot mechanism the Scope topic's Context objects use internally). Reading `count` in `app.js` always re-reads the live slot in `counter.js`; there is no intermediate copy anywhere to go stale. This is a fundamentally different mechanism from CommonJS's `module.exports = {...}`, which really does hand out a reference to one specific object — mutating a *property* of that object is visible to importers, but reassigning `module.exports` itself afterward is not retroactively seen by anyone who already imported it.

## 10. Common Pitfalls

1. **Forgetting the file extension** in relative import paths for native ESM (`./math` fails; `./math.js` is required in browsers and Node ESM, unlike bundler-based setups which often tolerate omitting it).
2. **Mixing default and named import syntax incorrectly** — `import { default as X }` works but is unusual; prefer `import X from "..."` for default exports.
3. **Assuming imports are copies** — mutating an imported live binding's *source* elsewhere updates every importer; this differs from CommonJS's snapshot behavior and can surprise developers moving between the two systems.
4. **Circular imports** — two modules importing from each other can result in one side seeing an incomplete (partially initialized) module during the first evaluation pass; restructure to avoid true circular dependencies where possible.

## 11. Quick Reference

```
export const x = 1;           export function f(){}      export default class {}
import { x, f } from "./m.js";  import Default from "./m.js";  import * as ns from "./m.js";
export { x as renamed };        import { x as local };
```
