Read OSS

Rolldown's Architecture: A Map to the Codebase

Intermediate

Prerequisites

  • Basic familiarity with Rust ownership and traits
  • Conceptual understanding of how JavaScript bundlers work (modules, tree shaking, code splitting)
  • Familiarity with Rollup or esbuild at the user level

Rolldown's Architecture: A Map to the Codebase

Rolldown is a JavaScript/TypeScript bundler written in Rust, designed to be the future bundler for Vite. It aims for Rollup API compatibility with esbuild-class speed — a combination that requires careful architectural choices. Before diving into any implementation details in later articles, this first installment gives you the conceptual map you need to navigate the codebase confidently.

The project sits at roughly 55+ Rust crates and several TypeScript packages, organized in a hybrid monorepo. That sounds imposing, but the architecture follows a clean three-layer pattern that makes the whole system navigable once you understand the overall shape.

The Three-Layer Architecture

Rolldown separates concerns across three distinct layers, each with clear responsibilities:

flowchart TB
    subgraph TS["TypeScript API Layer"]
        direction LR
        rolldown["rolldown()"]
        build["build()"]
        watch["watch()"]
    end

    subgraph NAPI["NAPI-RS Binding Layer"]
        direction LR
        BindingBundler["BindingBundler"]
        ClassicBundler["ClassicBundler"]
    end

    subgraph Rust["Rust Core"]
        direction LR
        BundleFactory["BundleFactory"]
        Bundle["Bundle"]
        Pipeline["Scan → Link → Generate"]
    end

    TS -->|"Options normalization + FFI"| NAPI
    NAPI -->|"Creates bundles"| Rust

Layer 1: TypeScript API (packages/rolldown/src/) — The public surface that developers interact with. It exports rolldown(), build(), watch(), and all TypeScript types. This layer handles plugin option hooks, normalizes user-supplied configuration, and converts everything into binding-compatible types before crossing the FFI boundary.

You can see the full public API surface in packages/rolldown/src/index.ts#L1-L225 — note the Rollup-compatible type aliases like RolldownError as RollupError on line 209.

Layer 2: NAPI-RS Binding (crates/rolldown_binding/) — The bridge between JavaScript and Rust. This layer initializes the Tokio async runtime with custom thread sizing, sets up mimalloc as the global allocator, installs a panic hook, and provides the BindingBundler NAPI class that wraps ClassicBundler.

The binding entry point at crates/rolldown_binding/src/lib.rs#L83-L117 reveals key performance decisions: the Tokio runtime uses num_cpus::get_physical() * 3/2 worker threads because rolldown puts heavy blocking tasks in worker threads rather than the default blocking thread pool.

Layer 3: Rust Core (crates/rolldown/) — The pure bundler implementation. It exports Bundler, BundleFactory, Bundle, and BundleHandle — the ownership-driven API that enforces single-use bundle semantics. The core knows nothing about NAPI or JavaScript.

The Rust core's public API is deliberately minimal, as seen in crates/rolldown/src/lib.rs#L21-L39:

pub use crate::{
  bundle::{
    bundle::Bundle,
    bundle_factory::{BundleFactory, BundleFactoryOptions},
    bundle_handle::BundleHandle,
  },
  bundler::Bundler,
  bundler_builder::BundlerBuilder,
  types::{bundle_output::BundleOutput, bundler_config::BundlerConfig},
};

Tip: This three-layer design means you can study the bundling algorithm in pure Rust without NAPI noise, and you can understand the JavaScript API without reading any Rust. Start with whichever layer matches your expertise.

Directory Structure Walkthrough

The repository is a hybrid monorepo managed with both pnpm (Node.js) and cargo (Rust). Here's a map of the key areas:

Directory Purpose
crates/rolldown/ Rust core: bundler logic, pipeline stages, AST scanner, module loader
crates/rolldown_binding/ NAPI-RS bindings exposing Rust to Node.js
crates/rolldown_plugin/ Plugin trait definition and PluginDriver dispatch
crates/rolldown_plugin_* ~30 built-in plugins (Vite compatibility, replace, analyzer, etc.)
crates/rolldown_common/ Shared types: modules, symbols, options, chunks
crates/rolldown_watcher/ Watch mode coordinator (actor-pattern state machine)
packages/rolldown/ Main npm package: TypeScript API, CLI, plugin utilities
packages/rolldown-tests/ Vitest test suite for the Node.js API
packages/rollup-tests/ Rollup compatibility test suite
meta/design/ 11 spec-driven design documents

The Cargo workspace at the repository root declares all crates:

[workspace]
members = ["./crates/*", "tasks/*"]
resolver = "3"

Within crates/rolldown/src/, the core is organized by function:

Module Purpose
stages/scan_stage.rs Module discovery and parsing
stages/link_stage/ Symbol resolution, tree shaking, wrapping
stages/generate_stage/ Code splitting, chunk rendering, output
module_loader/ Async module loading with Tokio channels
ast_scanner/ OXC AST visitor extracting imports/exports
bundle/ BundleFactory, Bundle, BundleHandle ownership lifecycle
bundler/ Long-lived Bundler for watch/HMR mode
module_finalizers/ AST rewriting for scope hoisting
ecmascript/format/ Output format renderers (ESM, CJS, IIFE, UMD)

The Three-Stage Pipeline at a Glance

Every rolldown build passes through three sequential stages. Each stage produces a well-defined output that becomes the input to the next:

flowchart LR
    Scan["**Scan Stage**<br/>Module discovery<br/>& parsing"]
    Link["**Link Stage**<br/>Symbol resolution<br/>& tree shaking"]
    Gen["**Generate Stage**<br/>Code splitting<br/>& rendering"]

    Scan -->|"NormalizedScanStageOutput<br/>(ModuleTable, SymbolRefDb,<br/>EntryPoints, Runtime)"| Link
    Link -->|"LinkStageOutput<br/>(sorted modules, metas,<br/>used symbols)"| Gen
    Gen -->|"BundleOutput<br/>(assets, warnings)"| Output["Final files"]

Scan Stage (scan_stage.rs) discovers the entire module graph by resolving entry points and recursively following imports. It runs plugin hooks (resolveId, load, transform) for each module, parses them with OXC, and extracts metadata through the AstScanner. The output is NormalizedScanStageOutput, containing the complete ModuleTable, SymbolRefDb, entry points, and runtime module information.

You can see the output structure defined at crates/rolldown/src/stages/scan_stage.rs#L40-L54:

pub struct NormalizedScanStageOutput {
  pub module_table: ModuleTable,
  pub index_ecma_ast: IndexEcmaAst,
  pub entry_points: Vec<EntryPoint>,
  pub symbol_ref_db: SymbolRefDb,
  pub runtime: RuntimeModuleBrief,
  pub warnings: Vec<BuildDiagnostic>,
  // ... additional fields
}

Link Stage (link_stage/mod.rs) runs 13 ordered analysis passes over the module graph: sorting, CJS/ESM classification, module wrapping, symbol binding, tree shaking, and cross-module optimization. The output is LinkStageOutput, a fully annotated module graph ready for code generation.

The 13-pass sequence is visible in the link() method at crates/rolldown/src/stages/link_stage/mod.rs#L197-L211:

pub fn link(mut self) -> LinkStageOutput {
    self.sort_modules();
    self.compute_tla();
    self.determine_module_exports_kind();
    self.determine_safely_merge_cjs_ns();
    self.wrap_modules();
    // ... 6 more passes
    self.include_statements(&unreachable_import_expression_addrs);
    self.patch_module_dependencies();

Generate Stage (generate_stage/mod.rs) transforms the linked module graph into final output assets. It splits code into chunks using a BitSet reachability algorithm, computes cross-chunk imports/exports, deconflicts symbol names, finalizes ASTs for scope hoisting, and renders chunks in the requested output format.

Design Docs and Project Navigation

Rolldown follows a spec-driven development practice. The meta/design/ directory contains 11 design documents that capture architectural decisions and design intent:

Design Doc Topic
rust-bundler.md Core Bundler vs. ClassicBundler design rationale
rust-classic-bundler.md ClassicBundler API compatibility approach
watch-mode.md Watch mode actor pattern, state machine, debounce rules
code-splitting.md Code splitting algorithm and chunk optimization
manual-code-splitting.md User-configurable chunk grouping
module-id.md Module identification and stable ID generation
cli.md CLI argument parsing and configuration
devtools.md Debug tracing and devtools integration

The project's AGENTS.md (also symlinked as CLAUDE.md) serves as the canonical project bible. It defines the three-layer architecture, repository structure, bash commands, and contributor conventions. If you're ever lost, start there.

Tip: When code implements something described in a design doc, the codebase convention is to add a comment referencing it (e.g., // See meta/design/watch-mode.md). Grep for these references to find where design decisions are implemented.

Hybrid Monorepo Tooling

The hybrid pnpm/Cargo monorepo is orchestrated by just, a command runner. The justfile at the repository root provides unified commands across both ecosystems:

just roll        # Build, lint, and test everything (Rust + Node.js + repo)
just build-rolldown  # Build the rolldown node package + binding binary
just test-rust   # Test Rust crates with auto-snapshot updates
just test-node   # Test Node.js packages with Vitest
just lint        # Lint everything

The Rust workspace uses Cargo with strict clippy lints — pedantic is enabled by default with specific exceptions. The Node.js side uses pnpm workspaces with vite-plus (vp) for package management.

One subtle but important detail: the NAPI binding binary (packages/rolldown/src/binding.js and binding.d.ts) is auto-generated by just build-rolldown. These files should never be edited manually — the source of truth is the Rust code in crates/rolldown_binding/.

What's Next

With this map in hand, you're ready to dive deeper. In the next article, we'll trace the complete journey of a JavaScript API call through the NAPI boundary into Rust, exploring the options normalization pipeline and the dual bundler design that makes both Rollup compatibility and incremental rebuilds possible.