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:

  1. 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.

  2. 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

CratePurpose
rolldownMain bundler entry point. Contains Bundle, BundlerBuilder, and the three pipeline stages (scan, link, generate).
rolldown_commonShared types: Module, ModuleTable, SymbolRefDb, BundlerOptions, EntryPoint, chunk types. Used by almost every other crate.
rolldown_bindingnapi-rs bridge. Exposes BindingBundler with generate(), write(), scan(), close() to JS. Handles option normalization and tokio runtime setup.
rolldown_ecmascriptECMAScript AST wrapper around OXC. Contains EcmaAst type and arena-based AST cloning for incremental builds.
rolldown_ecmascript_utilsUtilities for working with ECMAScript ASTs.

Module Resolution and Plugins

CratePurpose
rolldown_resolverWrapper around oxc_resolver. Creates specialized resolvers per context: import, require, CSS, new URL(). Handles platform-specific resolution (browser vs node).
rolldown_pluginPlugin 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_utilsShared 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.

CrateWhat it replaces
rolldown_plugin_vite_resolveVite’s module resolution logic (bare specifiers, file URLs, externals, package.json handling).
rolldown_plugin_vite_aliasVite’s path alias resolution (resolve.alias config).
rolldown_plugin_vite_cssCSS file loading and compilation (CSS modules, preprocessors via callback to JS).
rolldown_plugin_vite_css_postPost-processing of CSS output.
rolldown_plugin_vite_jsonJSON file imports with named exports support.
rolldown_plugin_vite_htmlHTML entry point handling.
rolldown_plugin_vite_html_inline_proxyInline script extraction from HTML.
rolldown_plugin_vite_assetStatic asset handling (images, fonts, etc.).
rolldown_plugin_vite_asset_import_meta_urlnew URL('...', import.meta.url) pattern support.
rolldown_plugin_vite_build_import_analysisimport.meta.glob and dynamic import analysis for production builds.
rolldown_plugin_vite_dynamic_import_varsDynamic import with variable patterns (import(\./${name}.js`)`).
rolldown_plugin_vite_import_globimport.meta.glob() expansion.
rolldown_plugin_vite_manifestBuild manifest generation.
rolldown_plugin_vite_module_preload_polyfillModule preload polyfill injection.
rolldown_plugin_vite_load_fallbackFallback file loading.
rolldown_plugin_vite_react_refresh_wrapperReact Fast Refresh boundary wrapping.
rolldown_plugin_vite_reporterBuild progress reporting.
rolldown_plugin_vite_transformVite’s transform pipeline coordination.
rolldown_plugin_vite_wasm_fallbackWASM file handling.
rolldown_plugin_vite_web_worker_postWeb Worker post-processing.

Other Bundler Plugins

CratePurpose
rolldown_plugin_replaceString replacement (like @rollup/plugin-replace).
rolldown_plugin_data_urlData URL module support.
rolldown_plugin_hmrHot Module Replacement support.
rolldown_plugin_lazy_compilationLazy compilation for dev mode.
rolldown_plugin_isolated_declarationTypeScript isolated declarations (.d.ts emit).
rolldown_plugin_oxc_runtimeOXC runtime helpers injection.
rolldown_plugin_esm_external_requireESM-compatible external require handling.
rolldown_plugin_copy_moduleModule copying.
rolldown_plugin_chunk_import_mapImport map generation for chunks.
rolldown_plugin_bundle_analyzerBundle size analysis.

Infrastructure

CratePurpose
rolldown_fsFilesystem abstraction (enables testing with virtual FS).
rolldown_fs_watcherFile system watching for dev/watch mode.
rolldown_watcherHigh-level watcher orchestration.
rolldown_sourcemapSource map types (wraps oxc_sourcemap).
rolldown_errorError types: BuildDiagnostic, BuildResult, severity levels.
rolldown_tracingTracing/profiling setup (Chrome trace format).
rolldown_utilsGeneral utilities: bit sets, hashing, rayon helpers, path manipulation.
rolldown_std_utilsStandard library extension traits.
rolldown_workspaceWorkspace path detection.
rolldown_devtoolsDev tools integration (action tracing for debugging).
rolldown_devtools_actionAction types for devtools tracing.
string_wizardString manipulation with source map tracking (like magic-string).

Testing

CratePurpose
rolldown_testingTest infrastructure.
rolldown_testing_configTest configuration types.
rolldown_devDevelopment-only utilities.
rolldown_dev_commonShared dev utilities.
benchBenchmarking harness.

JS Packages

PackagePurpose
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
  • manualChunks for explicit grouping
  • preserveModules for 1:1 module-to-file output

Where it Intentionally Diverges

  • transformAst hook: A Rolldown-specific hook that gives plugins access to the OXC AST directly, enabling Rust-native AST transforms without serialization overhead.
  • experimental options: Features like devMode, incremental builds, and nativeMagicString that 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 mimalloc as its global allocator, runs a custom tokio runtime with tuned thread counts (num_cpus * 3/2 worker threads), and parallelizes module processing with rayon. These are implementation details that don’t affect the API but mean behavior under load differs.
  • HookUsage registration: Plugins declare which hooks they implement via register_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

  1. BindingBundler is a napi-rs class (annotated with #[napi]) that wraps the internal ClassicBundler. It exposes generate(), write(), scan(), and close() to JavaScript.

  2. Option normalization: JS options (BindingBundlerOptions) are converted to Rust options via normalize_binding_options(). This handles type conversions, default values, and plugin instantiation.

  3. Async execution: Methods like generate() return PromiseRaw using env.spawn_future(). The Rust bundler runs on tokio, and napi-rs bridges tokio futures to JS promises.

  4. 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");
  1. Global allocator: On non-WASM platforms, Rolldown uses mimalloc instead of the system allocator for better performance.

  2. Plugin callbacks: JS plugins are wrapped as SharedPluginable objects. When the Rust bundler needs to call a plugin hook, it invokes a napi ThreadsafeFunction that calls back into JS. This is the main source of Rust-JS boundary crossings during a build.

  3. Error handling: Rust BuildDiagnostic errors are converted to BindingError with 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:

  • ViteAliasPlugin implements resolve_id to handle resolve.alias config
  • ViteJsonPlugin implements transform to convert JSON files to ES modules with named exports
  • ViteCSSPlugin implements load and transform for CSS file processing (with a callback to JS for preprocessor compilation)
  • ViteResolvePlugin is a full module resolver with bare specifier handling, exports field 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:

  1. 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.
  2. Internal plugins run at native speed. The 20 crates above eliminate the JS overhead for Vite’s core functionality.
  3. 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.ts with 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.