Lab 10: Module Federation

Time: 60 minutes | Level: Architect | Docker: docker run -it --rm node:20-alpine sh

Understanding how Node.js resolves and caches modules is essential for building modular systems, dynamic plugins, and micro-frontend architectures. This lab covers ESM internals, vm.Module, and the painful world of CJS↔ESM interop.


Step 1: ESM Module System Internals

ESM uses a three-phase lifecycle:

  1. Parse — find all static imports, build module graph

  2. Link — resolve specifiers, create bindings

  3. Evaluate — execute module bodies in dependency order

app.mjs
  └── import './lib.mjs'          ← resolved at parse time (static)
        └── import './utils.mjs'  ← recursive
  └── const mod = await import(x) ← dynamic import: resolved at runtime

Key differences from CommonJS:

  • ESM is live bindings (not copies) — exported values update in place

  • ESM is asynchronous — top-level await is supported

  • ESM has import.meta (URL, resolve, dirname equivalent)


Step 2: import.meta — The ESM Context Object

💡 Always use fileURLToPath(import.meta.url) for __filename/__dirname equivalents in ESM. Never hardcode paths.


Step 3: Dynamic Import with Assertions


Step 4: Module Graph Caching

💡 Unlike CJS, ESM module cache cannot be easily cleared. This is intentional for better tree-shaking and analyzer support.


Step 5: vm.Module — SyntheticModule & SourceTextModule

Run: node --experimental-vm-modules vm-modules.mjs

📸 Verified Output:


Step 6: CJS ↔ ESM Interop — The Gotchas


Step 7: Dual-Mode Package (CJS + ESM)

💡 Always provide both "import" and "require" in "exports". This is the standard for dual-mode packages (tsup, esbuild, rollup can build both automatically).


Step 8: Capstone — Plugin System with vm.Module

Build a dynamic plugin loader using vm.Module:

Run: node --experimental-vm-modules plugin-system.mjs


Summary

Concept
API
Key Point

ESM live bindings

export let x

Updates propagate to importers

Module context

import.meta.url

Self-referential module URL

CJS in ESM

createRequire(import.meta.url)

Access CJS require() from ESM

ESM in CJS

await import(spec)

Dynamic import (async only)

SyntheticModule

new SyntheticModule(exports, fn)

Expose JS as ESM

SourceTextModule

new SourceTextModule(code)

Evaluate JS string as ESM

Dual-mode package

"exports" in package.json

Serve CJS + ESM from one package

Cache busting

Query string on URL

Force reload dynamic ESM

Last updated