Module Bundling Concepts
build-tooling1-1's own import example let a bundler figure out load order automatically — this chapter explains exactly how. Every bundler, regardless of brand, works from the same two ideas: an entry point, and the dependency graph it grows from.
The Entry Point — Where a Bundler Starts
You tell a bundler one (or a few) starting file — the entry point. The bundler then follows every import statement it finds, recursively, discovering the full set of files the application actually needs. Nothing outside that reachable set gets included at all — a detail worth remembering now, since it's exactly what build-tooling1-8's own tree-shaking chapter builds on directly.
The Dependency Graph
Each file is a node; each import is an edge pointing at a dependency. A small example:
Starting from app.js, the bundler discovers api.js and utils.js, and — since api.js itself imports utils.js — recognizes utils.js only needs to be included once in the final output, even though two different files reference it.
assembly1-9's own two-pass assembler: Pass 1 scans the whole file to build a complete symbol table before Pass 2 ever generates real machine code. A bundler does the same thing one level up — discover the entire graph first, then emit the actual bundled output — for the same underlying reason: you can't safely resolve or optimize something you haven't fully seen yet.
CommonJS vs. ES Modules — Not Just a Syntax Difference
build-tooling1-1 already contrasted CommonJS's require() against ESM's import. The real difference goes deeper than spelling:
CommonJS's require() is resolved dynamically, at runtime — it can sit inside an if statement, use a computed string, anything ordinary JavaScript can do. ES Modules' import/export are static — required to appear at a file's top level, with a fixed, literal module specifier, never conditional or computed. That constraint is deliberate: it means a tool can determine a file's entire set of dependencies just by parsing the text, without ever executing a single line of the program.
| CommonJS | ES Modules | |
|---|---|---|
| Resolution timing | Dynamic — at runtime | Static — determinable by parsing alone |
| Can appear conditionally? | Yes — inside if/for/functions | No — top-level only |
| Module specifier | Can be a computed/dynamic string | Must be a fixed, literal string |
| Full graph knowable without running the code? | Not reliably | Yes, always |
Why This Matters Beyond Syntax
A dependency graph built from ESM's static imports can be safely trimmed of any branch the entry point never actually reaches — build-tooling1-8's own tree-shaking depends entirely on this. A graph built from CommonJS can't be trimmed with the same confidence: since a require() call might be hidden behind a runtime condition, "unused" code identified by static analysis alone risks removing something a conditional path genuinely needed. This is exactly why modern npm packages increasingly ship dual builds — both a CommonJS version for older Node-style consumers, and a separate ES Module version specifically so modern bundlers can analyze and optimize it properly.
Coding Challenges
Using this chapter's own three-file example (app.js, api.js, utils.js), explain why utils.js only appears once in the final bundled output even though two different files import it.
📄 View solutionUsing this chapter's own dynamic-require example, explain why a bundler cannot reliably determine CommonJS's full dependency graph just by parsing the file's text, and explain why this determines whether tree-shaking is safe to perform.
📄 View solutionExplain the parallel this chapter draws between building a dependency graph and assembly1-9's own two-pass assembler algorithm — what's the structurally similar step in each, and why does each one need that step to happen first?
📄 View solutionChapter 2 Quick Reference
- Every bundler works from an entry point, following imports recursively to discover the full dependency graph
- Files reachable more than once (shared dependencies) are only included once in the final output
- Building the graph before generating output mirrors assembly1-9's own two-pass assembler — discover everything first, then generate
- CommonJS — require() is dynamic/conditional, resolved at runtime; the full graph can't always be known without running the code
- ES Modules — import/export are static and top-level-only; the full graph is always knowable from parsing alone
- This static-vs-dynamic distinction is exactly why tree-shaking (Ch.8) works reliably with ESM but not with CommonJS
- Many modern npm packages ship both a CommonJS and an ESM build — real, current practice, not a historical footnote
- Next chapter: Webpack — The Original Do-Everything Bundler