Plan: react_compiler_oxc — OXC Frontend for React Compiler
Context
The Rust React Compiler (compiler/crates/) currently accepts Babel-format AST (react_compiler_ast::File) + scope info (ScopeInfo) and compiles via compile_program(). The only frontend is a Babel NAPI bridge (compiler/packages/babel-plugin-react-compiler-rust/). This plan adds an OXC frontend that enables both build-time code transformation and linting via the OXC ecosystem, all in pure Rust (no JS/NAPI boundary).
Crate Structure
compiler/crates/react_compiler_oxc/
Cargo.toml
src/
lib.rs — Public API: transform(), lint(), ReactCompilerRule
prefilter.rs — Quick check for React-like function names in OXC AST
convert_ast.rs — OXC AST → react_compiler_ast::File
convert_ast_reverse.rs — react_compiler_ast → OXC AST (for applying results)
convert_scope.rs — OXC Semantic → ScopeInfo
diagnostics.rs — CompileResult → OxcDiagnostic conversion
Dependencies (Cargo.toml)
[dependencies]
react_compiler_ast = { path = "../react_compiler_ast" }
react_compiler = { path = "../react_compiler" }
react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
oxc_parser = "..."
oxc_ast = "..."
oxc_semantic = "..."
oxc_allocator = "..."
oxc_span = "..."
oxc_diagnostics = "..."
oxc_linter = "..." # for Rule trait
indexmap = "..."
Module Details
1. prefilter.rs — Quick React Function Check
Port of babel-plugin-react-compiler-rust/src/prefilter.ts.
pub fn has_react_like_functions(program: &oxc_ast::ast::Program) -> bool
- Use
oxc_ast::Visittrait to walk the AST - Check
FunctionDeclarationnames,VariableDeclaratorinits that are arrow/function expressions - Skip class bodies
- Name check:
starts_with(uppercase)or matchesuse[A-Z0-9] - Return
trueon first match (early exit)
2. convert_scope.rs — OXC Semantic → ScopeInfo
pub fn convert_scope_info(semantic: &oxc_semantic::Semantic) -> ScopeInfo
This is the most natural conversion — both use arena-indexed flat tables with copyable u32 IDs.
Scopes: Iterate semantic.scopes(). For each scope:
ScopeId— direct u32 remappingparent— fromscope_tree.get_parent_id()kind— mapScopeFlags→ScopeKind(Top→Program, Function→Function, CatchClause→Catch, etc.; use parent AST node to distinguish For vs Block)bindings— fromscope_tree.get_bindings(), map name→SymbolId to name→BindingId
Bindings: Iterate semantic.symbols(). For each symbol:
BindingId— direct u32 remapping from SymbolIdname,scope— direct from SymbolTablekind— inspect declaration AST node type: VariableDeclaration(var/let/const), FunctionDeclaration→Hoisted, param→Param, ImportDeclaration→Moduledeclaration_type— string name of the declaring AST node typedeclaration_start— span.start of the binding's declaring identifierimport— for Module bindings, extract source/kind/imported from the ImportDeclaration
node_to_scope: Walk AST nodes that create scopes; map node.span().start → ScopeId.
reference_to_binding: Iterate all references from SymbolTable. For each resolved reference: map reference.span().start → BindingId. Also add each symbol's declaration identifier span.
program_scope: ScopeId(0).
Key files:
- Target types:
compiler/crates/react_compiler_ast/src/scope.rs - Reference impl:
compiler/packages/babel-plugin-react-compiler-rust/src/scope.ts
3. convert_ast.rs — OXC AST → react_compiler_ast::File
pub fn convert_program(
program: &oxc_ast::ast::Program,
source_text: &str,
comments: &[oxc_ast::Comment],
) -> react_compiler_ast::File
Approach: Recursive conversion, one function per AST category (statement, expression, pattern, JSX, etc.). Data is copied out of OXC's arena into owned react_compiler_ast types.
ConvertCtx: Holds a line-offset table (built from source_text at init) for computing Position { line, column, index } from byte offsets.
BaseNode construction:
start = Some(span.start),end = Some(span.end)— critical for scope lookupsloc— computed via line-offset table binary search
Key mappings:
| OXC | react_compiler_ast |
|---|---|
Statement enum variants |
statements::Statement variants |
Expression enum variants |
expressions::Expression variants |
Declaration (separate in OXC) |
Folded into Statement (Babel style) |
BindingPattern |
patterns::PatternLike |
JSXElement/Fragment/etc |
jsx::* types |
| TS type annotations | Option<Box<serde_json::Value>> (opaque passthrough) |
Comments: Map OXC Comment { kind, span } → react_compiler_ast::common::Comment (CommentBlock/CommentLine with start/end/value).
Key files:
- Target types:
compiler/crates/react_compiler_ast/src/(all modules)
4. convert_ast_reverse.rs — react_compiler_ast → OXC AST
Mirror of convert_ast.rs. Converts the compiled Babel-format AST back into OXC AST nodes.
pub fn convert_program_to_oxc<'a>(
file: &react_compiler_ast::File,
allocator: &'a oxc_allocator::Allocator,
) -> oxc_ast::ast::Program<'a>
- Allocates new OXC AST nodes into the provided arena
- Maps each
react_compiler_asttype back to its OXC equivalent - The
CompileResult::Success { ast, .. }returnsast: Option<serde_json::Value>— first deserialize toreact_compiler_ast::File, then convert to OXC
This is the most labor-intensive module but avoids the perf cost of re-parsing.
5. diagnostics.rs — Compiler Results → OXC Diagnostics
pub fn compile_result_to_diagnostics(
result: &CompileResult,
source_text: &str,
) -> Vec<oxc_diagnostics::OxcDiagnostic>
Map compiler events/errors to OXC diagnostics:
LoggerEvent::CompileError { fn_loc, detail }→OxcDiagnostic::warn/errorwith label at fn_loc spanCompileResult::Error { error, .. }→OxcDiagnostic::error- Preserve error messages and source locations
6. lib.rs — Public API
Transform API (build pipeline)
/// Result of compiling a program
pub struct TransformResult<'a> {
/// The compiled program (None if no changes needed)
pub program: Option<oxc_ast::ast::Program<'a>>,
pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
pub events: Vec<LoggerEvent>,
}
/// Primary API — accepts pre-parsed AST + semantic
pub fn transform<'a>(
program: &oxc_ast::ast::Program,
semantic: &oxc_semantic::Semantic,
source_text: &str,
comments: &[oxc_ast::Comment],
options: PluginOptions,
output_allocator: &'a oxc_allocator::Allocator,
) -> TransformResult<'a>
/// Convenience wrapper — parses from source text
pub fn transform_source<'a>(
source_text: &str,
source_type: oxc_span::SourceType,
options: PluginOptions,
output_allocator: &'a oxc_allocator::Allocator,
) -> TransformResult<'a>
Flow:
- Prefilter (
has_react_like_functions). Skip ifcompilationMode == "all". - Convert AST (
convert_program) - Convert scope (
convert_scope_info) - Call
compile_program(file, scope_info, options) - On success with modified AST: deserialize JSON →
File, reverse-convert to OXC AST - Convert diagnostics
Lint API
pub struct LintResult {
pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
}
/// Lint — accepts pre-parsed AST + semantic
pub fn lint(
program: &oxc_ast::ast::Program,
semantic: &oxc_semantic::Semantic,
source_text: &str,
comments: &[oxc_ast::Comment],
options: PluginOptions,
) -> LintResult
/// Convenience wrapper
pub fn lint_source(
source_text: &str,
source_type: oxc_span::SourceType,
options: PluginOptions,
) -> LintResult
Same as transform but with no_emit = true / lint output mode. Only collects diagnostics, no AST output.
oxc_linter::Rule Implementation
pub struct ReactCompilerRule {
options: PluginOptions,
}
impl oxc_linter::Rule for ReactCompilerRule {
fn run_once(&self, ctx: &LintContext) {
// ctx already has parsed AST + semantic
let result = lint(
ctx.program(),
ctx.semantic(),
ctx.source_text(),
ctx.comments(),
self.options.clone(),
);
for diagnostic in result.diagnostics {
ctx.diagnostic(diagnostic);
}
}
}
This avoids double-parsing since oxc_linter provides pre-parsed AST and semantic analysis.
Implementation Phases
Phase 1: Foundation (convert_scope + convert_ast + prefilter)
convert_scope.rswith unit tests comparing against Babel scope extractionconvert_ast.rswith unit tests comparing against Babel parser JSON outputprefilter.rswith simple true/false tests- These are independently testable without the full pipeline
Phase 2: Lint path (diagnostics + lint API + Rule)
diagnostics.rslint()function inlib.rsReactCompilerRuleimpl- Test against existing compiler fixtures — verify diagnostics match
Phase 3: Transform path (reverse converter + transform API)
convert_ast_reverse.rstransform()function inlib.rs- Integration tests: compile fixtures through OXC pipeline, compare output with Babel pipeline
Phase 4: Differential testing
- Cross-validate AST conversion: parse same source with both Babel and OXC, convert both to
react_compiler_ast::File, diff - Cross-validate scope conversion: compare
ScopeInfofrom both paths - Run full fixture suite through both pipelines, compare compiled output
Verification
- Unit tests: Each module has tests for its conversion logic
- Fixture tests: Use existing fixtures at
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ - Differential tests: Compare OXC path output against Babel path output for same inputs
cargo test -p react_compiler_oxc— run all crate tests- Scope correctness: Most critical — incorrect scope info causes wrong compilation. Snapshot
ScopeInfoJSON and compare against Babel extraction golden files