| 1 | # Plan: `react_compiler_oxc` — OXC Frontend for React Compiler |
| 2 | |
| 3 | ## Context |
| 4 | |
| 5 | 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). |
| 6 | |
| 7 | ## Crate Structure |
| 8 | |
| 9 | ``` |
| 10 | compiler/crates/react_compiler_oxc/ |
| 11 | Cargo.toml |
| 12 | src/ |
| 13 | lib.rs — Public API: transform(), lint(), ReactCompilerRule |
| 14 | prefilter.rs — Quick check for React-like function names in OXC AST |
| 15 | convert_ast.rs — OXC AST → react_compiler_ast::File |
| 16 | convert_ast_reverse.rs — react_compiler_ast → OXC AST (for applying results) |
| 17 | convert_scope.rs — OXC Semantic → ScopeInfo |
| 18 | diagnostics.rs — CompileResult → OxcDiagnostic conversion |
| 19 | ``` |
| 20 | |
| 21 | ### Dependencies (Cargo.toml) |
| 22 | |
| 23 | ```toml |
| 24 | [dependencies] |
| 25 | react_compiler_ast = { path = "../react_compiler_ast" } |
| 26 | react_compiler = { path = "../react_compiler" } |
| 27 | react_compiler_diagnostics = { path = "../react_compiler_diagnostics" } |
| 28 | oxc_parser = "..." |
| 29 | oxc_ast = "..." |
| 30 | oxc_semantic = "..." |
| 31 | oxc_allocator = "..." |
| 32 | oxc_span = "..." |
| 33 | oxc_diagnostics = "..." |
| 34 | oxc_linter = "..." # for Rule trait |
| 35 | indexmap = "..." |
| 36 | ``` |
| 37 | |
| 38 | ## Module Details |
| 39 | |
| 40 | ### 1. `prefilter.rs` — Quick React Function Check |
| 41 | |
| 42 | Port of `babel-plugin-react-compiler-rust/src/prefilter.ts`. |
| 43 | |
| 44 | ```rust |
| 45 | pub fn has_react_like_functions(program: &oxc_ast::ast::Program) -> bool |
| 46 | ``` |
| 47 | |
| 48 | - Use `oxc_ast::Visit` trait to walk the AST |
| 49 | - Check `FunctionDeclaration` names, `VariableDeclarator` inits that are arrow/function expressions |
| 50 | - Skip class bodies |
| 51 | - Name check: `starts_with(uppercase)` or matches `use[A-Z0-9]` |
| 52 | - Return `true` on first match (early exit) |
| 53 | |
| 54 | ### 2. `convert_scope.rs` — OXC Semantic → ScopeInfo |
| 55 | |
| 56 | ```rust |
| 57 | pub fn convert_scope_info(semantic: &oxc_semantic::Semantic) -> ScopeInfo |
| 58 | ``` |
| 59 | |
| 60 | This is the most natural conversion — both use arena-indexed flat tables with copyable u32 IDs. |
| 61 | |
| 62 | **Scopes:** Iterate `semantic.scopes()`. For each scope: |
| 63 | - `ScopeId` — direct u32 remapping |
| 64 | - `parent` — from `scope_tree.get_parent_id()` |
| 65 | - `kind` — map `ScopeFlags` → `ScopeKind` (Top→Program, Function→Function, CatchClause→Catch, etc.; use parent AST node to distinguish For vs Block) |
| 66 | - `bindings` — from `scope_tree.get_bindings()`, map name→SymbolId to name→BindingId |
| 67 | |
| 68 | **Bindings:** Iterate `semantic.symbols()`. For each symbol: |
| 69 | - `BindingId` — direct u32 remapping from SymbolId |
| 70 | - `name`, `scope` — direct from SymbolTable |
| 71 | - `kind` — inspect declaration AST node type: VariableDeclaration(var/let/const), FunctionDeclaration→Hoisted, param→Param, ImportDeclaration→Module |
| 72 | - `declaration_type` — string name of the declaring AST node type |
| 73 | - `declaration_start` — span.start of the binding's declaring identifier |
| 74 | - `import` — for Module bindings, extract source/kind/imported from the ImportDeclaration |
| 75 | |
| 76 | **`node_to_scope`:** Walk AST nodes that create scopes; map `node.span().start → ScopeId`. |
| 77 | |
| 78 | **`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. |
| 79 | |
| 80 | **`program_scope`:** `ScopeId(0)`. |
| 81 | |
| 82 | Key files: |
| 83 | - Target types: `compiler/crates/react_compiler_ast/src/scope.rs` |
| 84 | - Reference impl: `compiler/packages/babel-plugin-react-compiler-rust/src/scope.ts` |
| 85 | |
| 86 | ### 3. `convert_ast.rs` — OXC AST → react_compiler_ast::File |
| 87 | |
| 88 | ```rust |
| 89 | pub fn convert_program( |
| 90 | program: &oxc_ast::ast::Program, |
| 91 | source_text: &str, |
| 92 | comments: &[oxc_ast::Comment], |
| 93 | ) -> react_compiler_ast::File |
| 94 | ``` |
| 95 | |
| 96 | **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. |
| 97 | |
| 98 | **ConvertCtx:** Holds a line-offset table (built from source_text at init) for computing `Position { line, column, index }` from byte offsets. |
| 99 | |
| 100 | **BaseNode construction:** |
| 101 | - `start = Some(span.start)`, `end = Some(span.end)` — critical for scope lookups |
| 102 | - `loc` — computed via line-offset table binary search |
| 103 | |
| 104 | **Key mappings:** |
| 105 | | OXC | react_compiler_ast | |
| 106 | |-----|-------------------| |
| 107 | | `Statement` enum variants | `statements::Statement` variants | |
| 108 | | `Expression` enum variants | `expressions::Expression` variants | |
| 109 | | `Declaration` (separate in OXC) | Folded into `Statement` (Babel style) | |
| 110 | | `BindingPattern` | `patterns::PatternLike` | |
| 111 | | `JSXElement/Fragment/etc` | `jsx::*` types | |
| 112 | | TS type annotations | `Option<Box<serde_json::Value>>` (opaque passthrough) | |
| 113 | |
| 114 | **Comments:** Map OXC `Comment { kind, span }` → `react_compiler_ast::common::Comment` (CommentBlock/CommentLine with start/end/value). |
| 115 | |
| 116 | Key files: |
| 117 | - Target types: `compiler/crates/react_compiler_ast/src/` (all modules) |
| 118 | |
| 119 | ### 4. `convert_ast_reverse.rs` — react_compiler_ast → OXC AST |
| 120 | |
| 121 | Mirror of `convert_ast.rs`. Converts the compiled Babel-format AST back into OXC AST nodes. |
| 122 | |
| 123 | ```rust |
| 124 | pub fn convert_program_to_oxc<'a>( |
| 125 | file: &react_compiler_ast::File, |
| 126 | allocator: &'a oxc_allocator::Allocator, |
| 127 | ) -> oxc_ast::ast::Program<'a> |
| 128 | ``` |
| 129 | |
| 130 | - Allocates new OXC AST nodes into the provided arena |
| 131 | - Maps each `react_compiler_ast` type back to its OXC equivalent |
| 132 | - The `CompileResult::Success { ast, .. }` returns `ast: Option<serde_json::Value>` — first deserialize to `react_compiler_ast::File`, then convert to OXC |
| 133 | |
| 134 | This is the most labor-intensive module but avoids the perf cost of re-parsing. |
| 135 | |
| 136 | ### 5. `diagnostics.rs` — Compiler Results → OXC Diagnostics |
| 137 | |
| 138 | ```rust |
| 139 | pub fn compile_result_to_diagnostics( |
| 140 | result: &CompileResult, |
| 141 | source_text: &str, |
| 142 | ) -> Vec<oxc_diagnostics::OxcDiagnostic> |
| 143 | ``` |
| 144 | |
| 145 | Map compiler events/errors to OXC diagnostics: |
| 146 | - `LoggerEvent::CompileError { fn_loc, detail }` → `OxcDiagnostic::warn/error` with label at fn_loc span |
| 147 | - `CompileResult::Error { error, .. }` → `OxcDiagnostic::error` |
| 148 | - Preserve error messages and source locations |
| 149 | |
| 150 | ### 6. `lib.rs` — Public API |
| 151 | |
| 152 | #### Transform API (build pipeline) |
| 153 | |
| 154 | ```rust |
| 155 | /// Result of compiling a program |
| 156 | pub struct TransformResult<'a> { |
| 157 | /// The compiled program (None if no changes needed) |
| 158 | pub program: Option<oxc_ast::ast::Program<'a>>, |
| 159 | pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>, |
| 160 | pub events: Vec<LoggerEvent>, |
| 161 | } |
| 162 | |
| 163 | /// Primary API — accepts pre-parsed AST + semantic |
| 164 | pub fn transform<'a>( |
| 165 | program: &oxc_ast::ast::Program, |
| 166 | semantic: &oxc_semantic::Semantic, |
| 167 | source_text: &str, |
| 168 | comments: &[oxc_ast::Comment], |
| 169 | options: PluginOptions, |
| 170 | output_allocator: &'a oxc_allocator::Allocator, |
| 171 | ) -> TransformResult<'a> |
| 172 | |
| 173 | /// Convenience wrapper — parses from source text |
| 174 | pub fn transform_source<'a>( |
| 175 | source_text: &str, |
| 176 | source_type: oxc_span::SourceType, |
| 177 | options: PluginOptions, |
| 178 | output_allocator: &'a oxc_allocator::Allocator, |
| 179 | ) -> TransformResult<'a> |
| 180 | ``` |
| 181 | |
| 182 | Flow: |
| 183 | 1. Prefilter (`has_react_like_functions`). Skip if `compilationMode == "all"`. |
| 184 | 2. Convert AST (`convert_program`) |
| 185 | 3. Convert scope (`convert_scope_info`) |
| 186 | 4. Call `compile_program(file, scope_info, options)` |
| 187 | 5. On success with modified AST: deserialize JSON → `File`, reverse-convert to OXC AST |
| 188 | 6. Convert diagnostics |
| 189 | |
| 190 | #### Lint API |
| 191 | |
| 192 | ```rust |
| 193 | pub struct LintResult { |
| 194 | pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>, |
| 195 | } |
| 196 | |
| 197 | /// Lint — accepts pre-parsed AST + semantic |
| 198 | pub fn lint( |
| 199 | program: &oxc_ast::ast::Program, |
| 200 | semantic: &oxc_semantic::Semantic, |
| 201 | source_text: &str, |
| 202 | comments: &[oxc_ast::Comment], |
| 203 | options: PluginOptions, |
| 204 | ) -> LintResult |
| 205 | |
| 206 | /// Convenience wrapper |
| 207 | pub fn lint_source( |
| 208 | source_text: &str, |
| 209 | source_type: oxc_span::SourceType, |
| 210 | options: PluginOptions, |
| 211 | ) -> LintResult |
| 212 | ``` |
| 213 | |
| 214 | Same as transform but with `no_emit = true` / lint output mode. Only collects diagnostics, no AST output. |
| 215 | |
| 216 | #### oxc_linter::Rule Implementation |
| 217 | |
| 218 | ```rust |
| 219 | pub struct ReactCompilerRule { |
| 220 | options: PluginOptions, |
| 221 | } |
| 222 | |
| 223 | impl oxc_linter::Rule for ReactCompilerRule { |
| 224 | fn run_once(&self, ctx: &LintContext) { |
| 225 | // ctx already has parsed AST + semantic |
| 226 | let result = lint( |
| 227 | ctx.program(), |
| 228 | ctx.semantic(), |
| 229 | ctx.source_text(), |
| 230 | ctx.comments(), |
| 231 | self.options.clone(), |
| 232 | ); |
| 233 | for diagnostic in result.diagnostics { |
| 234 | ctx.diagnostic(diagnostic); |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | ``` |
| 239 | |
| 240 | This avoids double-parsing since oxc_linter provides pre-parsed AST and semantic analysis. |
| 241 | |
| 242 | ## Implementation Phases |
| 243 | |
| 244 | ### Phase 1: Foundation (convert_scope + convert_ast + prefilter) |
| 245 | - `convert_scope.rs` with unit tests comparing against Babel scope extraction |
| 246 | - `convert_ast.rs` with unit tests comparing against Babel parser JSON output |
| 247 | - `prefilter.rs` with simple true/false tests |
| 248 | - These are independently testable without the full pipeline |
| 249 | |
| 250 | ### Phase 2: Lint path (diagnostics + lint API + Rule) |
| 251 | - `diagnostics.rs` |
| 252 | - `lint()` function in `lib.rs` |
| 253 | - `ReactCompilerRule` impl |
| 254 | - Test against existing compiler fixtures — verify diagnostics match |
| 255 | |
| 256 | ### Phase 3: Transform path (reverse converter + transform API) |
| 257 | - `convert_ast_reverse.rs` |
| 258 | - `transform()` function in `lib.rs` |
| 259 | - Integration tests: compile fixtures through OXC pipeline, compare output with Babel pipeline |
| 260 | |
| 261 | ### Phase 4: Differential testing |
| 262 | - Cross-validate AST conversion: parse same source with both Babel and OXC, convert both to `react_compiler_ast::File`, diff |
| 263 | - Cross-validate scope conversion: compare `ScopeInfo` from both paths |
| 264 | - Run full fixture suite through both pipelines, compare compiled output |
| 265 | |
| 266 | ## Verification |
| 267 | |
| 268 | 1. **Unit tests:** Each module has tests for its conversion logic |
| 269 | 2. **Fixture tests:** Use existing fixtures at `compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/` |
| 270 | 3. **Differential tests:** Compare OXC path output against Babel path output for same inputs |
| 271 | 4. **`cargo test -p react_compiler_oxc`** — run all crate tests |
| 272 | 5. **Scope correctness:** Most critical — incorrect scope info causes wrong compilation. Snapshot `ScopeInfo` JSON and compare against Babel extraction golden files |