rolldown/rolldown
github.com/rolldown/rolldown ↗ · 3998 files · 6.3 MB · scan capped
[![MIT licensed][badge-license]][url-license] [![NPM version][badge-npm-version]][url-npm] [![Discord chat][badge-discord]][discord-url] [ ][url-npm] Rolldown Rolldown is a JavaScript/TypeScript bundler written in Rust intended to serve as the future bundler u
Overview
Rolldown is a Rust-based JavaScript/TypeScript bundler for application developers and tooling authors, exposing Rollup-compatible APIs through TypeScript and Node-API bindings. Its core separates scanning, linking, and output generation, with distinct lifecycle models for independent builds and persistent watch/dev sessions. The inspected source also exposes browser/WASI packaging, parallel plugins, compiler utilities, and experimental Vite integration APIs. This brief describes commit 37d60ff510b4b326f4b587c27f28c606d76373e8 through bounded source inspection; it includes no executed test or performance results.
System map
The implementation crosses TypeScript, Node-API, and Rust boundaries. The public package supports independent builds, while watch and dev APIs introduce longer-lived runtime owners. Persistent state in the inspected architecture consists primarily of module/cache structures and filesystem artifacts. Distribution includes native and WASI/browser builds, with separate documentation deployment.
- The documented architecture places the TypeScript API above crates/rolldown_binding and the Rust core; the public entry exports build, defineConfig, rolldown, and watch.
- The CLI executable enables Node's compile cache when available, schedules an unreferenced cache flush, then imports the compiled CLI entry.
- ClassicBundler creates a fresh BundleFactory per operation; the core Bundler architecture retains its factory and ScanStageCache across builds.
- Bundle owns filesystem access, shared options/resolver/emitter, a plugin driver, warnings, and a scan cache; its output pipeline calls LinkStage followed by GenerateStage.
- The Rust workspace includes crates and tasks and uses edition 2024; the root Node manifest pins pnpm 11.24.0 and requires Node ^20.19.0 or >=22.12.0.
- Oxc provides configured AST, transformer, minifier, semantic, and codegen capabilities; napi enables async/tracing integration, and release builds use fat LTO with one codegen unit.
- just test-rust runs workspace tests excluding the binding crate; Node recipes build the package and route to Rolldown and Rollup suites, with separate browser, WASI, and WebContainer commands.
- Native/browser/WASI package builds are separate scripts; npm publishing uses the release environment and OIDC provenance permission, while VitePress documentation deploys through a main-only Void job.
Feature catalog
The user-facing product is chiefly a CLI and a programmatic bundling API. Output operations support memory generation and filesystem writing, complemented by plugins and compiler utilities. Experimental surfaces expose development-engine callbacks, browser memory filesystems, and Vite-oriented integrations. The following catalog covers inspected capabilities rather than asserting an exhaustive option inventory.
- rolldown(input) returns a Rollup-compatible bundle object whose generate() produces in-memory output and write() produces disk output.
- The experimental build() convenience API defaults write to true, supports write:false, accepts arrays, and closes each individual build in finally.
- The CLI handles help/version, config-based or positional-input bundling, and --environment assignments; without an output destination it prints generated code/assets, while watch requires an output destination.
- Configuration discovery supports rolldown.config with .js/.mjs/.cjs/.ts/.mts/.cts extensions; config functions receive raw CLI arguments, and native versus bundle loaders select execution paths.
- watch() rebuilds when modules change and exposes event/change/restart/close notifications; BUNDLE_END and ERROR events carry results with close support.
- Experimental DevEngine exposes a current module graph, HMR/output/additional-asset callbacks, rebuild strategy selection, and configurable polling/debounce/include/exclude watch settings.
- rolldown/utils exports synchronous/asynchronous parsing, minification, and transformation plus TsconfigCache and Visitor; experimental browser builds additionally expose memfs.
- Inspected builtins cover external-require conversion and replacement, plus experimental Vite resolve/JSON/glob/import-analysis/preload/worker/React-refresh/reporting/manifest/transform helpers and bundle analysis.
Data flow
Options pass through JavaScript validation and hook processing before becoming binding inputs. Sources arrive through plugin load hooks or filesystem reads and become module, AST, symbol, and statement tables. Linking determines retained behavior, and generation turns that state into chunks and assets. State lifetime and error handling differ between independent builds and retained watch sessions.
- rolldown() validates input before options hooks; each output operation validates output before createBundlerOptions combines the retained input and current output settings.
- createBundlerOptions normalizes input/output plugins, runs outputOptions hooks, builds PluginContextData, and returns binding input/output options, a parallel-plugin registry, and worker cleanup.
- load_source prioritizes plugin output, appends load sourcemaps and side-effect metadata, and uses asserted module types only when the plugin supplied no type.
- Without plugin source, load_source reads resolved paths as text or bytes according to module type; native reads use spawn_blocking, while wasm branches read directly through FileSystem.
- NormalizedScanStageOutput carries module and AST tables, per-module statement information, entry points, symbols, dynamic-import usage, and top-level-await metadata; snapshots clone ASTs into another arena and omit symbol scoping.
- LinkStage sorts modules and computes TLA/export kinds before wrapping, side-effect analysis, import/export binding, reference analysis, cross-module optimization, statement inclusion, and dependency patching.
- GenerateStage finalizes the chunk plan before sealing symbol liveness; it derives and commits cross-chunk links, then resolves file URLs, finalizes modules, and renders assets.
- After generation, additional emitted files and generateBundle mutations enter output; filenames are validated before writing, and writeBundle runs after filesystem writes. Binding build errors become BindingErrors, while successful results become RolldownOutputImpl.
Behavior traces
Three concrete flows were followed through their implementations: CLI config bundling, programmatic output generation/writing, and watch initialization/rebuild. The traces preserve meaningful branches such as native versus bundled config loading and generate versus write. Cleanup and recovery are part of the observed behavior, including worker/runtime release and watch registration after failed scans.
- CLI startup: bin/cli.mjs imports dist/cli.mjs → CLI checks Node compatibility → main calls parseCliArguments → applies environment assignments → selects help, version, config bundling, positional bundling, or help fallback.
- Config flow: bundleWithConfig sets watch environment flags when needed → loadConfig discovers/selects the loader → bundled TS configs pass through bundleTsConfig and dynamic import with unlink cleanup; native loading uses freshImport or a timestamped import.
- Config output flow: resolve the config function with rawArgs → bundleInner iterates configs → rolldown merges CLI input overrides → sequential build.write calls merge output overrides → finally build.close → output-size reporting.
- Programmatic flow: rolldown validates input and calls options hooks → constructs RolldownBuild/startAsyncRuntime → generate/write invokes #build → createBundlerOptions → BindingBundler.generate/write → ClassicBundler.create_bundle.
- Core output flow: Bundle.generate/write → scan_modules → ScanStage.scan/build_start/fetch_modules → build_end → bundle_up → LinkStage.link → GenerateStage.generate → additional files/generate_bundle → filename validation; write additionally performs filesystem writes/write_bundle.
- Build cleanup: RolldownBuild.close claims runtime release before awaiting → stops workers → awaits BindingBundler.close → releases the runtime in finally; asyncDispose delegates to close.
- Watch startup: watch returns WatcherEmitter while createWatcher asynchronously runs per-output options hooks/createBundlerOptions → constructs BindingWatcher → starts the runtime → schedules run on nextTick → coordinator emits START and builds each task.
- Watch rebuild: file messages enter debounce processing → run_build_sequence dispatches changes/watchChange/restart/START → affected tasks build; WatchTask clears resolution/tsconfig caches, scans, registers watches before checking scan errors, and selects bundle_generate when skipWrite is set.
Testing signals
Tests and CI were inspected, not executed; test existence is not a passing result. The inspected suites combine Rust fixture tests, Vitest API/CLI/watch tests, Mocha Rollup compatibility tests, and packed-browser smoke tests. Coverage is intentionally split across runtime and platform lanes, with conditional CI execution. This bounded inspection establishes particular assertions and limitations, not whole-repository coverage.
- Inspected, not executed: rolldown-tests uses Vitest with fork workers and a 20-second default timeout; its scripts separate main, watcher, CLI, WASI, stability, runtime-lifecycle, and typechecking tests.
- Build API tests assert repeated ESM/IIFE writes, concurrent ESM/CJS writes, async disposal/closed state, closeBundle error propagation, and closeBundle behavior when no output build occurred.
- CLI tests spawn the executable with execa and snapshot normalized help output; the inspected 'should not hang after running' basic case is skipped, and the Node-version case explicitly exercises supported versions only.
- Watch tests force polling by default, use one-second delays to cross mtime boundaries, and allow three retries; the inspected watch case checks update/create/delete hook notifications.
- Rust integration modules cover Rolldown fixtures, esbuild, Rollup, and test262; fixture macros discover _config.json files. The inspected TypeScript fixture runner selects sequential configs and supports skip/retry/before/after/error callbacks.
- Mocha runs the Rollup compatibility harness; a packed-browser Vitest test verifies browser export selection, absence of fetched Vite builtin stubs, virtual-plugin graph bundling, and leaf-before-consumer output ordering.
- Output safety has Rust unit cases for traversal/rooted paths and platform-specific drive/backslash behavior; cleanDir Vitest cases assert removal/preservation across writes and preservation of hook-created files.
- CI configuration was inspected, not run: change filters and labels gate platform/runtime lanes; browser and plain-Node fallback suites run for changed Node inputs, while WebContainer is conditional. Windows builds run on main or explicit opt-in; macOS builds are main-only.
Risk map
The main observed trust boundaries are executable configuration/plugins, output filesystem operations, worker IPC, and package acquisition. Source contains explicit protections and comments about lifecycle/deadlock constraints. The observations below identify behaviors and maintenance hazards; they do not establish exploitable vulnerabilities. Documentation and test limitations are scoped to the inspected files.
- Config loading executes imported JavaScript; default TS loading writes a hashed configuration bundle beside the original file and later unlinks it with ignored callback errors. It therefore requires filesystem and code-execution trust in the selected config.
- cleanDir removes entries recursively from the configured output directory before generation when no output.file is set; cleanup failures become warnings. Destination selection is consequently a high-impact filesystem boundary.
- Generated filenames are checked after generateBundle for escaping the output directory, and write rejects NUL bytes; these checks constrain bundler-emitted paths, while inspected plugin tests directly write files through Node fs.
- The WebContainer fallback uses a version-specific /tmp directory, removes/recreates it when the binding entry is missing, invokes pnpm i synchronously, and requires the installed binding. This introduces network/package-manager and outside-project filesystem effects on that branch.
- Parallel plugin workers dynamically import supplied file URLs; their adapter currently supplies empty input/output options, fresh isolated PluginContextData, and watchMode:false, with explicit TODOs for context sharing and watch metadata.
- Binding close deliberately uses a synchronous mutable-receiver method returning a spawned future because asynchronous NAPI mutable access is documented as unsafe; generation also pre-resolves external paths to avoid synchronous JS-callback deadlocks.
- CLI prototype-key filtering occurs after cac parsing; --environment writes process.env before help/version dispatch. These are observable input boundaries, not proof of complete pollution prevention or an identified exploit.
- Documentation drift is concrete: build() says array outputs run sequentially but uses Promise.all; the Rust bundler doc proposes removing close and describes cache-clearing behavior, whereas current close marks closed and closes the last handle.
Extension points
Plugins are the principal extension mechanism across JavaScript and Rust. Hook adapters preserve metadata and filters across the language boundary, while factories manage state lifetime. Additional APIs expose compiler utilities and experimental dev callbacks. CLI options derive from schema information rather than a standalone hand-maintained command inventory.
- JavaScript options hooks execute in pre/normal/post order and can replace input options; outputOptions hooks synchronously apply the same ordering model to output settings.
- bindingifyPlugin adapts build, output, and watch hook families; resolveId, transform, load, and renderChunk adapters return filter information alongside hook metadata.
- Rust plugins implement Plugin: Any + Debug + Send + Sync + 'static, provide a name, and optionally override default no-op/None build_start, resolve_id, load, transform, and module_parsed hooks.
- Builtin TypeScript factories construct named BuiltinPlugin descriptors; examples include import-glob, dynamic-import-vars, JSON, and resolver helpers, with some made callable through makeBuiltinPluginCallable.
- Parallel plugin descriptors carry index/fileUrl/options into a shared registry and up to eight workers; workers import implementations, instantiate per-thread plugins, and register adapted callbacks.
- CLI option registration iterates schemaInfo and alias metadata, skips never-typed entries, supports required/optional values, and registers a default variadic input command accepting unknown options for later warnings.
- Bundle is generic over a cloneable FileSystem and delegates source reads/writes through that interface; plugin-backed virtual inputs provide another integration route, demonstrated by the browser smoke test.
- DevEngine.create converts caller callbacks for HMR, full output, and additional assets into binding callbacks and supplies watch/rebuild controls; this is an inspected integration boundary for hosting development behavior.
Contributor handoff
Future agents should begin with feature-local implementation and lifecycle documentation, then verify it against code. Safe boundaries distinguish public TypeScript sources, NAPI adapters, generated bindings, and Rust core state owners. Tests and CI were inspected, not executed; this brief supplies no passing result and its inventory is bounded. Suggested checks below are for a separately authorized implementation session, not actions performed during this investigation.
- Read the relevant internal-docs feature first, keep it synchronized with implementation changes, and verify proposals against source; lifecycle docs explicitly separate bundler-level from per-build data.
- Do not directly edit binding.cjs, binding.d.cts, debug generated output, or Rollup/test262 submodules; regenerate bindings from Rust through the documented build workflow.
- For public API changes, follow rolldown/index.ts → RolldownBuild → createBundlerOptions → BindingBundler/ClassicBundler, preserving deferred graph construction and fresh-output-build semantics.
- For watch/cache changes, read WatchTask before modifying phase order: it clears resolver/tsconfig caches, registers scan-discovered watches before propagating errors, and branches on skipWrite.
- Follow the path guide's known-UTF-8 convention, consuming rolldown_std_utils helpers for owned paths, absolute explicit cwd arguments, and native separators for module IDs.
- In an authorized implementation session, prefer just recipes: targeted Node runs can use test-node-rolldown-only, Rust fixtures use t-run, and lint-rust includes formatting, locked all-feature/all-target checking, and Clippy with denied warnings.
- Tests and CI were inspected, not executed. Future validation must account for Rust snapshot updates, WASI replacing native dist, browser versus WebContainer fallback paths, and conditional platform lanes; listed test files do not establish a passing result.
- Respect repository compatibility and review conventions: rolldown_* Rust crates do not follow semver, PRs remain drafts until requested otherwise, and concurrent fixtures should use mocks rather than global expect.assertions.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 7100 | 76% |
| JSON | 2658 | docs/config |
| TypeScript | 1282 | 14% |
| Rust | 878 | 9% |
| Markdown | 240 | docs/config |
| CSS | 97 | docs/config |
| TOML | 62 | 1% |
| HTML | 45 | docs/config |
Start reading here
Probable source entrypoint: Cargo.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
crates/ (1817 files) packages/ (1814 files)
Source layout — crates/
| Path | Files |
|---|---|
crates/bench/ | 1 |
crates/rolldown/ | 263 |
Repository structure
| Top-level path | Files |
|---|---|
crates/ | 1817 |
packages/ | 1814 |
examples/ | 110 |
docs/ | 82 |
scripts/ | 45 |
.github/ | 37 |
internal-docs/ | 37 |
tasks/ | 18 |
Docs found: AGENTS.mdCONTRIBUTING.mdREADME.mddocs/.vitepress/theme/components/api.index.mddocs/README.mddocs/acknowledgements.mddocs/apis/bundler-api.mddocs/apis/cli.mddocs/apis/plugin-api.mddocs/apis/plugin-api/file-urls.md
Package
Cargo.toml — , 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructions.github/workflows/autofix.yml— GitHub Actions workflow.github/workflows/automate-jobs.yml— GitHub Actions workflow.github/workflows/automation-labeled.yml— GitHub Actions workflow.github/workflows/benchmark-node.yml— GitHub Actions workflow.github/workflows/benchmark-rust.yml— GitHub Actions workflow.github/workflows/ci.yml— GitHub Actions workflow.github/workflows/deploy-website.yml— GitHub Actions workflow.github/workflows/issue-close-require.yml— GitHub Actions workflow
Structural health signals
Average score 8.7/10 across 96 analyzed source files; 3 hotspots surfaced.
packages/rollup-tests/test/watch/index.js— 1795 lines, score 6.2 (Large file, Long function/block)packages/rollup-tests/test/hooks/index.js— 1247 lines, score 6.2 (Large file, Long function/block)scripts/src/esbuild-tests/gen-tests.ts— 517 lines, score 6.4 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
37d60ff510b42026-10-02 — test(inline-common-chunks): fix flaky inlineCommonChunks concurrent imports (#11088)7baaf3b461c42026-10-02 — fix(link): treat re-exports of the same external binding as one export (#11069)3db6853e64f92026-10-02 — fix: escape namespace properties in IIFE and UMD output (#11085)d184ee9059982026-10-02 — fix: escape dependency paths in CJS and UMD output (#11084)3d1a73fc8d442026-10-02 — fix: use computed access for arbitrary import names (#11083)