Navigating the Oxc Codebase: Architecture and Crate Map
Prerequisites
- ›Basic Rust knowledge (cargo workspaces, crates, modules)
- ›Familiarity with JavaScript tooling concepts (parsers, linters, bundlers)
Navigating the Oxc Codebase: Architecture and Crate Map
If you've ever stared at a JavaScript project's build pipeline and wondered, "could this be faster?", the Oxc project is the Rust-flavored answer. Oxc (The Oxidation Compiler) is a modular collection of high-performance JavaScript and TypeScript tools — parser, linter, transformer, minifier, formatter, and code generator — all written in Rust, sharing a single arena-allocated AST. It powers Rolldown (the Rust-based Vite bundler) and sits at the heart of the VoidZero ecosystem. This article is the first of six that will take you from a 10,000-foot view down to the individual optimization tricks that make Oxc fast.
Project Mission and Ecosystem Context
Oxc exists because JavaScript tooling is fundamentally CPU-bound, and Rust delivers predictable, zero-overhead performance. The project's stated goals from its architecture document are direct:
- Performance: Deliver 10–100× faster performance than existing JavaScript tools
- Correctness: Maintain full compatibility with ECMAScript and TypeScript standards
- Modularity: Enable users to compose tools to fit their needs
- Developer Experience: Provide excellent error messages and tooling integration
The key insight behind Oxc's architecture is sharing. Instead of each tool (parser, linter, transformer) maintaining its own AST representation — as ESLint, Babel, and Terser each do — Oxc defines a single AST, allocated in a single memory arena, and all tools operate on it in sequence. This eliminates serialization boundaries and redundant parsing.
flowchart LR
subgraph VoidZero Ecosystem
Oxc[Oxc Toolchain]
Rolldown[Rolldown Bundler]
Vite[Vite]
end
Oxc -->|parser, transformer, minifier| Rolldown
Rolldown -->|bundling| Vite
Oxc -->|linter| Oxlint[oxlint CLI]
Oxc -->|NAPI bindings| Node[Node.js Tools]
Repository Layout and Workspace Structure
The repository is organized into four top-level directories, each with a distinct role:
| Directory | Purpose | Examples |
|---|---|---|
apps/ |
End-user CLI binaries | oxlint, oxfmt |
crates/ |
Core library crates (publishable) | oxc_parser, oxc_linter, oxc_allocator |
napi/ |
Node.js NAPI bindings | parser, transform, minify |
tasks/ |
Development tools & code generators | ast_tools, coverage, benchmark |
The workspace is configured in Cargo.toml with Rust 2024 edition and resolver v3:
[workspace]
resolver = "3"
members = ["apps/*", "crates/*", "napi/*", "tasks/*"]
The workspace declares an MSRV of 1.92.0, following an N-2 policy (roughly 12 weeks behind latest stable). This balances contributors wanting bleeding-edge Rust features with downstream consumers needing stability.
The release profile at Cargo.toml#L272-L280 is tuned for maximum runtime performance — opt-level = 3, fat LTO, single codegen unit, and panic = "abort":
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = "symbols"
panic = "abort"
Tip: The
panic = "abort"setting is deliberate — it forces developers to write code that handles errors gracefully rather than relying on unwinding. If you see a panic in Oxc, it's a bug.
The Three-Tier Crate Architecture
Oxc organizes its ~31 crates into three tiers that mirror a classic compiler architecture: foundation, processing, and application. Understanding this layering is essential for navigating the codebase.
flowchart TB
subgraph Application["Application Layer"]
oxlint[oxlint CLI]
oxfmt[oxfmt CLI]
lsp[Language Server]
napi_parser[napi/parser]
napi_transform[napi/transform]
end
subgraph Processing["Processing Layer"]
parser[oxc_parser]
semantic[oxc_semantic]
linter[oxc_linter]
transformer[oxc_transformer]
minifier[oxc_minifier]
codegen[oxc_codegen]
mangler[oxc_mangler]
formatter[oxc_formatter]
end
subgraph Foundation["Foundation Layer"]
allocator[oxc_allocator]
ast[oxc_ast]
span[oxc_span]
syntax[oxc_syntax]
diagnostics[oxc_diagnostics]
end
Application --> Processing
Processing --> Foundation
Foundation Layer
These crates have zero or minimal dependencies on each other and define the types that every other crate uses:
oxc_allocator— The arena-based memory allocator. NoRc/Arcin hot paths.oxc_span— Source positions usingu32byte offsets (theSpantype), atoms, and source types.oxc_syntax— Token definitions, keyword mappings, operator types, scope/symbol ID types.oxc_diagnostics— Rich error reporting built onmiette, with multiple output formats.oxc_ast— The complete JavaScript/TypeScript AST definitions.
Processing Layer
These crates perform the actual compilation work, each consuming and producing AST data:
oxc_parser— Hand-written recursive descent parser for JS, TS, and JSX.oxc_semantic— Scope chain construction, symbol table, reference resolution.oxc_linter— 730+ lint rules across 15 plugin categories.oxc_transformer— Babel-compatible ES2015–ES2026 transforms, TypeScript stripping, JSX.oxc_minifier— Fixed-point peephole optimization loop, dead code elimination.oxc_codegen— AST-to-source printer with source map generation.oxc_mangler— Identifier shortening based on scope and frequency analysis.
Application Layer
These crates compose processing crates into user-facing tools. The umbrella crate crates/oxc/src/lib.rs uses feature flags to re-export sub-crates:
#[cfg(feature = "semantic")]
pub mod semantic {
pub use oxc_semantic::*;
}
#[cfg(feature = "transformer")]
pub mod transformer {
pub use oxc_transformer::*;
}
This design lets downstream consumers pull in only the features they need — a project using only the parser pays no compile-time cost for the linter or transformer.
The CompilerInterface Pipeline
The glue that ties all processing crates together is the CompilerInterface trait, defined in crates/oxc/src/compiler.rs. This trait defines a full compilation pipeline with hook points for customization.
The pipeline proceeds through seven stages, each optionally gated by a configuration method:
sequenceDiagram
participant User as Consumer
participant CI as CompilerInterface
participant P as Parser
participant S as SemanticBuilder
participant T as Transformer
participant C as Compressor
participant M as Mangler
participant G as Codegen
User->>CI: compile(source_text, source_type, path)
CI->>P: parse()
P-->>CI: ParserReturn (AST + errors)
CI->>CI: after_parse() hook
CI->>S: build(program)
S-->>CI: Scoping (scopes + symbols)
CI->>CI: after_semantic() hook
CI->>T: build_with_scoping()
T-->>CI: TransformerReturn
CI->>CI: after_transform() hook
CI->>C: build(program, options)
CI->>M: build(program, options)
CI->>G: build(program)
G-->>CI: CodegenReturn (code + source map)
CI->>CI: after_codegen() hook
The core of the compile method at compiler.rs#L117-L212 follows this exact sequence. Each stage is guarded by a corresponding options method — if transform_options() returns None, the transform phase is skipped entirely.
A crucial design detail: the pipeline threads a Scoping struct through every stage. This struct (which we'll explore in detail in Article 3) contains the scope tree, symbol table, and reference resolution data. It flows from semantic analysis, through transformation (which updates it), through injection/define plugins, and finally to the mangler and codegen:
let mut scoping = semantic_return.semantic.into_scoping();
// Transform updates scoping
if let Some(options) = self.transform_options() {
let mut transformer_return =
self.transform(options, &allocator, &mut program, source_path, scoping);
scoping = transformer_return.scoping;
}
Each hook method (after_parse, after_semantic, after_transform) returns a ControlFlow<()>, allowing consumers to abort the pipeline early. This makes CompilerInterface equally useful for a full build pipeline and a lint-only workflow that stops after semantic analysis.
Development Workflow and Tooling
Day-to-day development in Oxc revolves around the justfile, which defines standardized commands:
| Command | Purpose |
|---|---|
just ready |
Full CI check: format, lint, test, doc, AST codegen |
just ast |
Regenerate all code derived from AST definitions |
just test |
Run cargo test --all-features |
just fmt |
Format Rust + JS code, remove unused dependencies |
just new-rule name plugin |
Scaffold a new lint rule for any plugin |
The just ready command at justfile#L36-L47 runs everything CI will check, making it the single command to run before submitting a PR.
flowchart LR
A[just ready] --> B[typos]
B --> C[cargo lintgen]
C --> D[just fmt]
D --> E[just check]
E --> F[just test]
F --> G[just lint]
G --> H[just doc]
H --> I[just ast]
The just ast command is particularly important. It runs oxc_ast_tools, a code generator that reads annotated AST type definitions and produces visitor traits, builder methods, derive implementations, and layout assertions. This is not a proc macro — it's an ahead-of-time generator whose output is checked into git. We'll examine this system in depth in Article 4.
Tip: If
just astfails on a fresh clone, run it twice. The first pass generates Rust code that other generators depend on. The justfile handles this with a fallback:cargo run -p oxc_ast_tools || { cargo run -p oxc_ast_tools --no-default-features && cargo run -p oxc_ast_tools; }.
What's Ahead
This article gave you the map. The next five articles will take you into the territory. In Article 2, we'll dive deep into the foundation that makes everything fast: the arena allocator and AST design. You'll see how Box<'a, T> and Vec<'a, T> work without Drop, why the AST splits a single ESTree Identifier into three distinct types, and how the CloneIn and TakeIn traits enable safe mutation of arena-allocated data.