Rolldown Architecture
Rolldown is a JavaScript/TypeScript bundler written in Rust with a Node.js API exposed via napi-rs. It aims for Rollup API compatibility while delivering native-speed bundling. This document is based on reading the actual source code in the rolldown-rolldown repository (v1.0.0-rc.7).
Architecture Overview
Rolldown has two layers:
-
Rust core (
crates/rolldown/) - The bundler engine. Handles parsing (via OXC), module resolution, linking, tree shaking, code splitting, and code generation. This is where the performance comes from. Everything CPU-intensive runs here, parallelized with rayon and tokio. -
JS shell (
packages/rolldown/) - The Node.js-facing package. Handles configuration normalization, the Rollup-compatible plugin API surface, and option validation. It delegates all real work to the Rust core through napi-rs bindings.
The bridge between them (crates/rolldown_binding/) translates between JavaScript types and Rust types. It uses napi-rs to expose Rust structs as JavaScript classes and async Rust functions as JavaScript promises.
┌─────────────────────────────────────────────────┐
│ Node.js │
│ packages/rolldown/ (JS API, config, plugins) │
│ │ │
│ ▼ │
│ BindingBundler ← napi-rs bridge │
│ crates/rolldown_binding/ │
└────────────────────┬────────────────────────────┘
│ (native calls)
┌────────────────────▼────────────────────────────┐
│ Rust Core │
│ crates/rolldown/ │
│ ├── ScanStage (resolve + parse modules) │
│ ├── LinkStage (bind imports, tree shake) │
│ └── GenerateStage (chunk split, codegen) │
│ │
│ Supporting crates: │
│ rolldown_resolver, rolldown_plugin, │
│ rolldown_ecmascript, rolldown_common, ... │
└─────────────────────────────────────────────────┘
Crate Map
Core Bundler
| Crate | Purpose |
|---|---|
rolldown | Main bundler entry point. Contains Bundle, BundlerBuilder, and the three pipeline stages (scan, link, generate). |
rolldown_common | Shared types: Module, ModuleTable, SymbolRefDb, BundlerOptions, EntryPoint, chunk types. Used by almost every other crate. |
rolldown_binding | napi-rs bridge. Exposes BindingBundler with generate(), write(), scan(), close() to JS. Handles option normalization and tokio runtime setup. |
rolldown_ecmascript | ECMAScript AST wrapper around OXC. Contains EcmaAst type and arena-based AST cloning for incremental builds. |
rolldown_ecmascript_utils | Utilities for working with ECMAScript ASTs. |
Module Resolution and Plugins
| Crate | Purpose |
|---|---|
rolldown_resolver | Wrapper around oxc_resolver. Creates specialized resolvers per context: import, require, CSS, new URL(). Handles platform-specific resolution (browser vs node). |
rolldown_plugin | Plugin system. Defines the Plugin trait, Pluginable trait (async_trait wrapper), PluginDriver (orchestrates hook calls), and all hook types (resolve_id, load, transform, render_chunk, etc.). |
rolldown_plugin_utils | Shared utilities for plugins: CSS detection, public file handling, data-to-ESM conversion. |
Vite Integration Plugins
These crates implement Vite’s internal plugins in Rust. Each implements the Plugin trait.
| Crate | What it replaces |
|---|---|
rolldown_plugin_vite_resolve | Vite’s module resolution logic (bare specifiers, file URLs, externals, package.json handling). |
rolldown_plugin_vite_alias | Vite’s path alias resolution (resolve.alias config). |
rolldown_plugin_vite_css | CSS file loading and compilation (CSS modules, preprocessors via callback to JS). |
rolldown_plugin_vite_css_post | Post-processing of CSS output. |
rolldown_plugin_vite_json | JSON file imports with named exports support. |
rolldown_plugin_vite_html | HTML entry point handling. |
rolldown_plugin_vite_html_inline_proxy | Inline script extraction from HTML. |
rolldown_plugin_vite_asset | Static asset handling (images, fonts, etc.). |
rolldown_plugin_vite_asset_import_meta_url | new URL('...', import.meta.url) pattern support. |
rolldown_plugin_vite_build_import_analysis | import.meta.glob and dynamic import analysis for production builds. |
rolldown_plugin_vite_dynamic_import_vars | Dynamic import with variable patterns (import(\./${name}.js`)`). |
rolldown_plugin_vite_import_glob | import.meta.glob() expansion. |
rolldown_plugin_vite_manifest | Build manifest generation. |
rolldown_plugin_vite_module_preload_polyfill | Module preload polyfill injection. |
rolldown_plugin_vite_load_fallback | Fallback file loading. |
rolldown_plugin_vite_react_refresh_wrapper | React Fast Refresh boundary wrapping. |
rolldown_plugin_vite_reporter | Build progress reporting. |
rolldown_plugin_vite_transform | Vite’s transform pipeline coordination. |
rolldown_plugin_vite_wasm_fallback | WASM file handling. |
rolldown_plugin_vite_web_worker_post | Web Worker post-processing. |
Other Bundler Plugins
| Crate | Purpose |
|---|---|
rolldown_plugin_replace | String replacement (like @rollup/plugin-replace). |
rolldown_plugin_data_url | Data URL module support. |
rolldown_plugin_hmr | Hot Module Replacement support. |
rolldown_plugin_lazy_compilation | Lazy compilation for dev mode. |
rolldown_plugin_isolated_declaration | TypeScript isolated declarations (.d.ts emit). |
rolldown_plugin_oxc_runtime | OXC runtime helpers injection. |
rolldown_plugin_esm_external_require | ESM-compatible external require handling. |
rolldown_plugin_copy_module | Module copying. |
rolldown_plugin_chunk_import_map | Import map generation for chunks. |
rolldown_plugin_bundle_analyzer | Bundle size analysis. |
Infrastructure
| Crate | Purpose |
|---|---|
rolldown_fs | Filesystem abstraction (enables testing with virtual FS). |
rolldown_fs_watcher | File system watching for dev/watch mode. |
rolldown_watcher | High-level watcher orchestration. |
rolldown_sourcemap | Source map types (wraps oxc_sourcemap). |
rolldown_error | Error types: BuildDiagnostic, BuildResult, severity levels. |
rolldown_tracing | Tracing/profiling setup (Chrome trace format). |
rolldown_utils | General utilities: bit sets, hashing, rayon helpers, path manipulation. |
rolldown_std_utils | Standard library extension traits. |
rolldown_workspace | Workspace path detection. |
rolldown_devtools | Dev tools integration (action tracing for debugging). |
rolldown_devtools_action | Action types for devtools tracing. |
string_wizard | String manipulation with source map tracking (like magic-string). |
Testing
| Crate | Purpose |
|---|---|
rolldown_testing | Test infrastructure. |
rolldown_testing_config | Test configuration types. |
rolldown_dev | Development-only utilities. |
rolldown_dev_common | Shared dev utilities. |
bench | Benchmarking harness. |
JS Packages
| Package | Purpose |
|---|---|
packages/rolldown/ | Main npm package. JS API, CLI, plugin types, option normalization. |
packages/rollup-tests/ | Rollup test suite compatibility runner. |
packages/bench/ | JavaScript benchmarks. |
packages/browser/ | Browser build support. |
packages/pluginutils/ | @rollup/pluginutils compatible utilities. |
packages/vite-tests/ | Vite integration tests. |
packages/debug/ | Debug tooling. |
Bundling Pipeline
The pipeline has three stages, executed sequentially. The Bundle::bundle_up method in crates/rolldown/src/bundle/bundle.rs orchestrates them:
// Simplified from Bundle::bundle_up
let scan_stage_output = self.scan_modules(ScanMode::Full).await?;
let link_stage_output = LinkStage::new(scan_stage_output, &self.options).link();
let bundle_output = GenerateStage::new(&mut link_stage_output, ...).generate().await;
ASCII Pipeline Diagram
SCAN STAGE
==========
Entry points ──► ModuleLoader
│
┌───────────┼───────────┐
▼ ▼ ▼ (parallel per module)
resolve_id load transform
(plugins) (plugins) (plugins)
│ │ │
└───────────┼───────────┘
▼
OXC Parser (parse to AST)
│
▼
AST Scanner (extract imports/exports)
│
▼
Discover new dependencies ──► back to resolve_id
│
▼
ScanStageOutput
├── ModuleTable
├── IndexEcmaAst
├── EntryPoints
└── SymbolRefDb
LINK STAGE
==========
ScanStageOutput
│
▼
sort_modules() ──── topological sort
│
▼
compute_tla() ──── detect top-level await
│
▼
determine_module_exports_kind()
│
▼
wrap_modules() ──── CJS/ESM interop wrapping
│
▼
bind_imports_and_exports() ──── resolve symbol references
│
▼
create_exports_for_ecma_modules()
│
▼
reference_needed_symbols()
│
▼
include_statements() ──── TREE SHAKING
(determine_side_effects)
│
▼
patch_module_dependencies()
│
▼
LinkStageOutput
GENERATE STAGE
==============
LinkStageOutput
│
▼
generate_chunks() ──── CODE SPLITTING
(bit-set algorithm for shared modules)
│
▼
compute_cross_chunk_links()
│
▼
deconflict_chunk_symbols() ──── rename collisions
│
▼
finalize_modules() ──── AST transforms per output format
│
▼
render_chunk_to_assets()
├── EcmaGenerator (code from AST)
├── render_chunks (plugin hook)
├── augment_chunk_hash
├── minify_chunks (via OXC minifier)
└── finalize_assets (content hashing, filenames)
│
▼
BundleOutput (Vec<Output>)
Stage Details
Scan Stage (stages/scan_stage.rs): Starts from entry points and recursively discovers the module graph. Uses ModuleLoader with a channel-based task system. Each module goes through the plugin pipeline: resolve_id -> load -> transform -> OXC parse -> AST scan. The output is a ModuleTable (all modules indexed by ModuleIdx) and a SymbolRefDb (all symbols across the graph). Supports both full scans and partial/incremental scans.
Link Stage (stages/link_stage/mod.rs): A synchronous, single-pass stage that operates on the full module graph. The link() method runs a fixed sequence of sub-steps. Tree shaking happens here via include_statements(), which walks from entry point exports and marks reachable statements. The determine_side_effects step respects package.json#sideEffects. Cross-module optimization (constant inlining) also runs here.
Generate Stage (stages/generate_stage/mod.rs): Splits linked modules into chunks, resolves cross-chunk imports, and generates output code. Code splitting uses a bit-set algorithm: each module gets a bit-set indicating which entry points reach it, and modules with identical bit-sets land in the same chunk. Supports manual chunking via manualChunks and advanced chunk groups. Output format rendering (ESM, CJS, IIFE, UMD) happens in ecmascript/format/.
The Rollup Compatibility Layer
Rolldown aims to be a drop-in replacement for Rollup. Here is what that means concretely:
Same Plugin API
The Plugin trait in crates/rolldown_plugin/src/plugin.rs mirrors Rollup’s plugin hooks:
- Build hooks:
buildStart,resolveId,load,transform,moduleParsed,buildEnd - Generate hooks:
renderStart,banner/footer/intro/outro,renderChunk,augmentChunkHash,generateBundle,writeBundle,closeBundle - Watch hooks:
watchChange,closeWatcher
The JS-side Plugin type in packages/rolldown/src/plugin/ maps these to the Rollup plugin interface. Existing Rollup plugins that only use standard hooks work without changes.
The resolveDynamicImport hook exists but is explicitly deprecated, with guidance to use resolveId instead.
Same Output Formats
Rolldown supports the same output formats as Rollup, implemented in crates/rolldown/src/ecmascript/format/:
esm(ES modules)cjs(CommonJS)iife(immediately invoked function expression)umd(Universal Module Definition)
Same Chunk Splitting Behavior
The code_splitting.rs module implements Rollup-compatible chunk splitting:
- Entry chunks for each entry point
- Common chunks for modules shared across multiple entry points
manualChunksfor explicit groupingpreserveModulesfor 1:1 module-to-file output
Where it Intentionally Diverges
transformAsthook: A Rolldown-specific hook that gives plugins access to the OXC AST directly, enabling Rust-native AST transforms without serialization overhead.experimentaloptions: Features likedevMode, incremental builds, andnativeMagicStringthat have no Rollup equivalent.- Built-in plugins: Rolldown ships Vite’s internal plugins as Rust crates (the
rolldown_plugin_vite_*family), which is not something Rollup does. - Performance characteristics: Rolldown uses
mimallocas its global allocator, runs a custom tokio runtime with tuned thread counts (num_cpus * 3/2worker threads), and parallelizes module processing with rayon. These are implementation details that don’t affect the API but mean behavior under load differs. HookUsageregistration: Plugins declare which hooks they implement viaregister_hook_usage(), allowing the plugin driver to skip calling plugins for hooks they don’t use. This is an optimization not present in Rollup.
Rust-JS Bridge (napi-rs)
The rolldown_binding crate is the boundary between Rust and Node.js. Understanding this layer is essential for contributions that touch both sides.
How It Works
-
BindingBundleris a napi-rs class (annotated with#[napi]) that wraps the internalClassicBundler. It exposesgenerate(),write(),scan(), andclose()to JavaScript. -
Option normalization: JS options (
BindingBundlerOptions) are converted to Rust options vianormalize_binding_options(). This handles type conversions, default values, and plugin instantiation. -
Async execution: Methods like
generate()returnPromiseRawusingenv.spawn_future(). The Rust bundler runs on tokio, and napi-rs bridges tokio futures to JS promises. -
Custom tokio runtime: The module init function (
#[napi_derive::module_init]) creates a tuned tokio runtime:
// From crates/rolldown_binding/src/lib.rs
let rt = builder
.max_blocking_threads(max_blocking_threads) // default 4, not tokio's 512
.worker_threads(num_cpus::get_physical() * 3 / 2)
.enable_all()
.build()
.expect("Failed to create tokio runtime");
-
Global allocator: On non-WASM platforms, Rolldown uses
mimallocinstead of the system allocator for better performance. -
Plugin callbacks: JS plugins are wrapped as
SharedPluginableobjects. When the Rust bundler needs to call a plugin hook, it invokes a napiThreadsafeFunctionthat calls back into JS. This is the main source of Rust-JS boundary crossings during a build. -
Error handling: Rust
BuildDiagnosticerrors are converted toBindingErrorwith structured fields (message, code, location) that the JS side can display.
The JS Side
In packages/rolldown/src/api/rolldown/rolldown-build.ts, the RolldownBuild class holds a BindingBundler instance:
export class RolldownBuild {
#bundler: BindingBundler;
async generate(outputOptions = {}): Promise<RolldownOutput> {
// normalize options, create BindingBundlerOptions, call native
const output = await this.#bundler.generate(option.bundlerOptions);
return new RolldownOutputImpl(unwrapBindingResult(output));
}
}
Each call to generate() or write() passes the full merged options (input + output) to the native side. The native side creates a Bundle, runs all three stages, and returns the output.
Parallel Plugins
For CPU-intensive JS plugins, Rolldown supports parallel plugin execution using Node.js worker threads. The parallel_js_plugin_registry in the binding crate manages a pool of workers, each running its own copy of the plugin. This is opt-in via the parallel plugin API.
Vite Integration
The 20 rolldown_plugin_vite_* crates represent Vite’s internal plugins being rewritten in Rust. This is not just a theoretical exercise - it is the active migration path for Vite’s bundler transition from Rollup to Rolldown.
What These Plugins Do
In current Vite (v5/v6), internal features like alias resolution, CSS processing, JSON imports, HTML handling, asset management, and import analysis are implemented as JavaScript Rollup plugins. They run in Vite’s plugin pipeline.
In the Rolldown-powered Vite (targeting Vite 7+), these same features are implemented as native Rust plugins that implement the Plugin trait directly. For example:
ViteAliasPluginimplementsresolve_idto handleresolve.aliasconfigViteJsonPluginimplementstransformto convert JSON files to ES modules with named exportsViteCSSPluginimplementsloadandtransformfor CSS file processing (with a callback to JS for preprocessor compilation)ViteResolvePluginis a full module resolver with bare specifier handling,exportsfield support, and external detection
Each plugin is named with a builtin: prefix (e.g., "builtin:vite-alias", "builtin:vite-json"), distinguishing them from user-land JS plugins.
Performance Implications
When these plugins run in Rust, they avoid the Rust-to-JS-to-Rust round-trip that JS plugins require. The resolve_id hook is called for every import in the module graph, so moving Vite’s resolver to Rust eliminates potentially tens of thousands of cross-boundary calls per build.
Some plugins still call back into JS for operations that depend on Node.js APIs or complex JS libraries. The CSS plugin, for example, uses a callback to invoke PostCSS/Sass/Less processors that only exist in the JS ecosystem. The pattern is: do as much work as possible in Rust, call into JS only when necessary.
What This Means for Vite
When Vite completes its transition to Rolldown:
- Both dev and build use the same bundler. Currently Vite uses esbuild for dev (dependency pre-bundling) and Rollup for production builds. Rolldown replaces both.
- Internal plugins run at native speed. The 20 crates above eliminate the JS overhead for Vite’s core functionality.
- The plugin API stays the same. User-land Vite/Rollup plugins continue to work because Rolldown implements the same hook interface.
Your Projects Connection
ai-skills-sync (tsup / tsdown)
ai-skills-sync currently uses tsup, which uses esbuild under the hood. The Rolldown ecosystem includes tsdown, a tsup replacement that uses Rolldown instead of esbuild.
tsdown benefits from Rolldown’s architecture:
- Full Rollup plugin compatibility (tsup plugins can migrate)
- Tree shaking that matches Rollup’s behavior (esbuild’s tree shaking is less granular)
- Native TypeScript support via OXC (no separate tsc step needed for
.d.tswith isolated declarations)
When tsdown reaches stability, migrating ai-skills-sync from tsup would mean: same CLI interface pattern, better tree shaking, and alignment with the Vite ecosystem’s toolchain.
markdown-task-planner (electron-vite / Vite 7)
markdown-task-planner uses electron-vite, which wraps Vite. When Vite 7 ships with Rolldown as its bundler:
- Faster production builds: The 20 Vite plugins running in Rust eliminate JS overhead. Module resolution, CSS processing, and import analysis all run at native speed.
- Faster dev server startup: Rolldown replaces esbuild for dependency pre-bundling, with a single unified module graph instead of the current two-bundler architecture.
- No action required: Since electron-vite wraps Vite, the Rolldown transition happens transparently. You get the performance gains through a Vite version bump.
The key insight is that both tools converge on the same underlying engine. Whether you are building a CLI tool (tsdown) or an Electron app (Vite), Rolldown is the bundler running underneath.