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

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:

Bindings: Iterate semantic.symbols(). For each symbol:

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:

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:

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:

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>

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:

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:

  1. Prefilter (has_react_like_functions). Skip if compilationMode == "all".
  2. Convert AST (convert_program)
  3. Convert scope (convert_scope_info)
  4. Call compile_program(file, scope_info, options)
  5. On success with modified AST: deserialize JSON → File, reverse-convert to OXC AST
  6. 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)

Phase 2: Lint path (diagnostics + lint API + Rule)

Phase 3: Transform path (reverse converter + transform API)

Phase 4: Differential testing

Verification

  1. Unit tests: Each module has tests for its conversion logic
  2. Fixture tests: Use existing fixtures at compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/
  3. Differential tests: Compare OXC path output against Babel path output for same inputs
  4. cargo test -p react_compiler_oxc — run all crate tests
  5. Scope correctness: Most critical — incorrect scope info causes wrong compilation. Snapshot ScopeInfo JSON and compare against Babel extraction golden files