feat(transformer): run React Compiler as a feature-gated transform pass (#23201)

## Summary

Exposes the React Compiler from `oxc_transformer` behind a Cargo feature so callers no longer need a separate `oxc_react_compiler::transform` + scoping-rebuild step before transforming.

- **`oxc_transformer`**: new `react_compiler` Cargo feature (off by default) + `TransformOptions::react_compiler` field. When set, `Transformer::build_with_scoping` runs the React Compiler first (via a private `run_react_compiler` method), replacing the program and rebuilding scoping for the downstream transforms.
- **Opt-in**: the feature is **not** enabled by `oxc_transformer`'s defaults, nor by `oxc`'s `transformer`/`full` features — enabling the transformer does not pull the heavy `oxc_react_compiler` dependency. It is a separate `react_compiler` feature on `oxc` (which implies `transformer`); the napi transform binding opts in explicitly.
- **Dedupe**: removes the inline React-Compiler orchestration (and the `react_compiler_options()` hook) from `oxc`'s `CompilerInterface` and the napi transform binding; both now drive it through `TransformOptions::react_compiler`.
- **Diagnostics**: the compiler's errors and warnings are merged into `TransformerReturn::errors` (each keeps its severity; downstream filters). `compile()` reports them but no longer aborts codegen — the transformer always leaves a valid program, matching the documented "code may still be available even if there are errors" behaviour.
- **Links**: updates the React Compiler references to the merged upstream location (`react/react/tree/main/compiler`).

## Notes

Transform/codegen output is unchanged when the feature is off (the field defaults to `None` everywhere). The diagnostics behaviour change (no codegen abort) is conformance-safe — the transform-conformance harness keys off reported diagnostics, not printed output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-06-10 08:03:07 +00:00
1 parent 8edd23459d
commit ec266bb733
17 files changed
+3356 -84

No files matched your search

Generated
+1 -1
View File
@@ -1816,7 +1816,6 @@ dependencies = [
"oxc_mangler",
"oxc_minifier",
"oxc_parser",
"oxc_react_compiler",
"oxc_regular_expression",
"oxc_semantic",
"oxc_span",
@@ -2776,6 +2775,7 @@ dependencies = [
"oxc_diagnostics",
"oxc_ecmascript",
"oxc_parser",
"oxc_react_compiler",
"oxc_regular_expression",
"oxc_semantic",
"oxc_span",
+3 -2
View File
@@ -36,7 +36,6 @@ oxc_isolated_declarations = { workspace = true, optional = true }
oxc_mangler = { workspace = true, optional = true }
oxc_minifier = { workspace = true, optional = true }
oxc_parser = { workspace = true, features = [] }
oxc_react_compiler = { workspace = true, optional = true }
oxc_regular_expression = { workspace = true, optional = true }
oxc_semantic = { workspace = true, optional = true }
oxc_span = { workspace = true }
@@ -59,7 +58,9 @@ full = [
]
semantic = ["oxc_semantic"]
transformer = ["oxc_transformer", "oxc_transformer_plugins", "oxc_react_compiler"]
transformer = ["oxc_transformer", "oxc_transformer_plugins"]
# Experimental React Compiler pass; off by default (pulls the heavy `oxc_react_compiler` dep).
react_compiler = ["transformer", "oxc_transformer/react_compiler"]
minifier = ["oxc_mangler", "oxc_minifier"]
codegen = ["oxc_codegen", "oxc_codegen/sourcemap"]
mangler = ["oxc_mangler"]
+5 -48
View File
@@ -8,7 +8,6 @@ use oxc_isolated_declarations::{IsolatedDeclarations, IsolatedDeclarationsOption
use oxc_mangler::{MangleOptions, Mangler, ManglerReturn};
use oxc_minifier::{CompressOptions, Compressor};
use oxc_parser::{ParseOptions, Parser, ParserReturn};
use oxc_react_compiler::{self, PluginOptions as ReactCompilerOptions};
use oxc_semantic::{Scoping, SemanticBuilder, SemanticBuilderReturn, Stats};
use oxc_span::SourceType;
use oxc_transformer::{TransformOptions, Transformer, TransformerReturn};
@@ -71,13 +70,6 @@ pub trait CompilerInterface {
None
}
/// Options for the [React Compiler], which runs before all other transforms.
///
/// [React Compiler]: oxc_react_compiler
fn react_compiler_options(&self) -> Option<ReactCompilerOptions> {
None
}
fn define_options(&self) -> Option<ReplaceGlobalDefinesConfig> {
None
}
@@ -166,60 +158,25 @@ pub trait CompilerInterface {
let stats = semantic_return.semantic.stats();
let mut scoping = semantic_return.semantic.into_scoping();
/* React Compiler */
// Runs first, on the pristine AST, before every other transform.
let mut errors = Vec::new();
if let Some(options) = self.react_compiler_options() {
// `compiled` lives in `allocator`, not borrowed from `program`, so the
// reassignment below is sound.
let result = oxc_react_compiler::transform(&program, &allocator, options);
errors.extend(result.errors);
errors.extend(result.warnings);
if let Some(compiled) = result.program {
program = compiled;
// Rebuild scoping for downstream transforms.
scoping = SemanticBuilder::new()
.with_enum_eval(true)
.build(&program)
.semantic
.into_scoping();
}
}
/* Transform */
if let Some(options) = self.transform_options() {
let mut transformer_return =
self.transform(options, &allocator, &mut program, source_path, scoping);
if !transformer_return.errors.is_empty() {
errors.append(&mut transformer_return.errors);
self.handle_errors(errors);
return;
}
// The React Compiler always leaves a valid program (compiled on
// success, original on bail-out), so its diagnostics are reported but
// never abort codegen.
if !errors.is_empty() {
self.handle_errors(errors);
}
// Reported but non-fatal: the transformer always leaves a valid program.
self.handle_errors(mem::take(&mut transformer_return.errors));
if self.after_transform(&mut program, &mut transformer_return).is_break() {
return;
}
(scoping) = transformer_return.scoping;
} else if !errors.is_empty() {
// The React Compiler ran without a transform; surface its diagnostics
// but never abort — it always leaves a valid program.
self.handle_errors(errors);
scoping = transformer_return.scoping;
}
// The transformer leaves symbols and scopes out of sync with the AST;
// the parser, React Compiler, and any fresh semantic build leave them in
// sync. Track the state so each consumer below only rebuilds when needed.
// the parser and any fresh semantic build leave them in sync. Track the
// state so each consumer below only rebuilds when needed.
let transform = self.transform_options().is_some();
let mut scoping_dirty = transform;
+4 -3
View File
@@ -1,11 +1,12 @@
# oxc_react_compiler
#
# Native oxc integration for the Rust port of React Compiler (facebook/react#36173).
# Native oxc integration for the Rust port of the React Compiler:
# https://github.com/react/react/tree/main/compiler
#
# This crate owns the oxc <-> react_compiler_ast conversion layer. The React
# Compiler *core* crates are frontend-agnostic (they depend on serde/indexmap
# only, never on oxc), so we consume them from crates.io (a published MIT fork of
# facebook/react#36173) and keep the AST/scope conversion on our side, written
# the React Compiler) and keep the AST/scope conversion on our side, written
# against the live workspace oxc AST.
[package]
@@ -37,7 +38,7 @@ oxc_span = { workspace = true }
oxc_syntax = { workspace = true }
# React Compiler core crates — frontend-agnostic (no oxc deps). Published fork of
# facebook/react#36173 (MIT). `package` keeps the in-code name `react_compiler*`.
# the React Compiler (MIT). `package` keeps the in-code name `react_compiler*`.
react_compiler = { package = "forked_react_compiler", version = "0.1.4" }
react_compiler_ast = { package = "forked_react_compiler_ast", version = "0.1.4" }
react_compiler_hir = { package = "forked_react_compiler_hir", version = "0.1.4" }
+1 -1
View File
@@ -1,6 +1,6 @@
# Oxc React Compiler
oxc integration for the Rust port of the [React Compiler](https://github.com/facebook/react/pull/36173).
oxc integration for the Rust port of the [React Compiler](https://github.com/react/react/tree/main/compiler).
## Overview
@@ -1,8 +1,8 @@
//! # React Compiler Example
//!
//! Runs the Rust port of React Compiler ([facebook/react#36173]) over a file
//! through the oxc frontend (parse + semantic -> convert -> compile -> convert
//! back -> codegen) and prints the memoized output.
//! Runs the Rust port of the [React Compiler] over a file through the oxc
//! frontend (parse + semantic -> convert -> compile -> convert back -> codegen)
//! and prints the memoized output.
//!
//! ## Usage
//!
@@ -10,7 +10,7 @@
//! just example react_compiler MyFile.jsx
//! ```
//!
//! [facebook/react#36173]: https://github.com/facebook/react/pull/36173
//! [React Compiler]: https://github.com/react/react/tree/main/compiler
use std::path::Path;
+5
View File
@@ -34,6 +34,7 @@ oxc_data_structures = { workspace = true, features = [
] }
oxc_diagnostics = { workspace = true }
oxc_ecmascript = { workspace = true }
oxc_react_compiler = { workspace = true, optional = true }
oxc_regular_expression = { workspace = true }
oxc_semantic = { workspace = true }
oxc_span = { workspace = true }
@@ -56,3 +57,7 @@ insta = { workspace = true }
oxc_codegen = { workspace = true, features = ["sourcemap"] }
oxc_parser = { workspace = true }
pico-args = { workspace = true }
[features]
# Run the experimental React Compiler as the first transform pass.
react_compiler = ["dep:oxc_react_compiler"]
+38 -1
View File
@@ -10,7 +10,11 @@ use std::path::Path;
use oxc_allocator::{Allocator, TakeIn, Vec as ArenaVec};
use oxc_ast::{AstBuilder, ast::*};
use oxc_diagnostics::OxcDiagnostic;
#[cfg(feature = "react_compiler")]
use oxc_react_compiler::{PluginOptions, transform as react_compiler_transform};
use oxc_semantic::Scoping;
#[cfg(feature = "react_compiler")]
use oxc_semantic::SemanticBuilder;
use oxc_span::{GetSpan, SPAN};
use oxc_traverse::{ReusableTraverseCtx, Traverse, traverse_mut_with_ctx};
@@ -109,6 +113,8 @@ pub struct Transformer<'a> {
env: EnvOptions,
#[expect(dead_code)]
proposals: ProposalOptions,
#[cfg(feature = "react_compiler")]
react_compiler: Option<PluginOptions>,
}
impl<'a> Transformer<'a> {
@@ -124,6 +130,8 @@ impl<'a> Transformer<'a> {
jsx: options.jsx.clone(),
env: options.env,
proposals: options.proposals,
#[cfg(feature = "react_compiler")]
react_compiler: options.react_compiler.clone(),
}
}
@@ -134,6 +142,12 @@ impl<'a> Transformer<'a> {
program: &mut Program<'a>,
) -> TransformerReturn {
let allocator = self.allocator;
#[cfg(feature = "react_compiler")]
let (scoping, react_compiler_diagnostics) = self.run_react_compiler(scoping, program);
#[cfg(not(feature = "react_compiler"))]
let react_compiler_diagnostics: std::vec::Vec<OxcDiagnostic> = std::vec::Vec::new();
let ast_builder = AstBuilder::new(allocator);
self.state.source_type = program.source_type;
@@ -188,8 +202,31 @@ impl<'a> Transformer<'a> {
traverse_mut_with_ctx(&mut transformer, program, &mut reusable_ctx);
let (mut state, scoping) = reusable_ctx.into_state_and_scoping();
let helpers_used = state.helper_loader.used_helpers.drain().collect();
let mut errors = react_compiler_diagnostics;
errors.extend(state.take_errors());
#[expect(deprecated)]
TransformerReturn { errors: state.take_errors(), scoping, helpers_used }
TransformerReturn { errors, scoping, helpers_used }
}
#[cfg(feature = "react_compiler")]
fn run_react_compiler(
&mut self,
scoping: Scoping,
program: &mut Program<'a>,
) -> (Scoping, std::vec::Vec<OxcDiagnostic>) {
let Some(options) = self.react_compiler.take() else {
return (scoping, std::vec::Vec::new());
};
let result = react_compiler_transform(program, self.allocator, options);
let mut diagnostics = result.errors;
diagnostics.extend(result.warnings);
let Some(compiled) = result.program else {
return (scoping, diagnostics);
};
*program = compiled;
let scoping =
SemanticBuilder::new().with_enum_eval(true).build(program).semantic.into_scoping();
(scoping, diagnostics)
}
}
+11
View File
@@ -1,5 +1,8 @@
use std::path::PathBuf;
#[cfg(feature = "react_compiler")]
use oxc_react_compiler::PluginOptions;
use crate::{
ReactRefreshOptions,
common::helper_loader::{HelperLoaderMode, HelperLoaderOptions},
@@ -68,6 +71,10 @@ pub struct TransformOptions {
/// Helper loading configuration for generated runtime helpers.
pub helper_loader: HelperLoaderOptions,
/// Experimental [React Compiler](https://github.com/react/react/tree/main/compiler).
#[cfg(feature = "react_compiler")]
pub react_compiler: Option<PluginOptions>,
}
impl TransformOptions {
@@ -100,6 +107,8 @@ impl TransformOptions {
mode: HelperLoaderMode::Runtime,
..Default::default()
},
#[cfg(feature = "react_compiler")]
react_compiler: None,
}
}
@@ -302,6 +311,8 @@ impl TryFrom<&BabelOptions> for TransformOptions {
proposals: ProposalOptions::default(),
helper_loader,
plugins,
#[cfg(feature = "react_compiler")]
react_compiler: None,
})
}
}
@@ -1,6 +1,8 @@
mod enum_eval;
mod es_target;
mod helper_call;
#[cfg(feature = "react_compiler")]
mod react_compiler;
mod targets;
use std::path::Path;
@@ -0,0 +1,27 @@
use std::path::Path;
use oxc_allocator::Allocator;
use oxc_codegen::Codegen;
use oxc_parser::Parser;
use oxc_react_compiler::default_plugin_options;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{TransformOptions, Transformer};
#[test]
fn memoizes_component_through_transformer() {
let allocator = Allocator::default();
let source = "export function Greeting({ name }) {\n return <div>Hello {name}</div>;\n}\n";
let mut program = Parser::new(&allocator, source, SourceType::jsx()).parse().program;
let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
let options =
TransformOptions { react_compiler: Some(default_plugin_options()), ..Default::default() };
let ret = Transformer::new(&allocator, Path::new("greeting.jsx"), &options)
.build_with_scoping(scoping, &mut program);
assert!(ret.errors.is_empty(), "{:?}", ret.errors);
let code = Codegen::new().build(&program).code;
assert!(code.contains("react/compiler-runtime"), "missing runtime import:\n{code}");
assert!(code.contains("_c("), "component was not memoized:\n{code}");
}
+1 -1
View File
@@ -22,7 +22,7 @@ test = false
doctest = false
[dependencies]
oxc = { workspace = true, features = ["full"] }
oxc = { workspace = true, features = ["full", "react_compiler"] }
oxc_napi = { workspace = true }
oxc_react_compiler = { workspace = true } # build the React Compiler `PluginOptions`
oxc_sourcemap = { workspace = true, features = ["napi"] }
+2 -2
View File
@@ -344,7 +344,7 @@ export interface ReactCompilerGating {
}
/**
* Options for the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
* Options for the experimental [React Compiler](https://github.com/react/react/tree/main/compiler).
*
* Mirrors the compiler's `PluginOptions`. The deep `environment` configuration
* (inference / validation flags) is not surfaced here.
@@ -601,7 +601,7 @@ export interface TransformOptions {
/** Decorator plugin */
decorator?: DecoratorOptions
/**
* Enable the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
* Enable the experimental [React Compiler](https://github.com/react/react/tree/main/compiler).
*
* `true` enables it with default options; an object enables it with the
* given options; `false` or omitted disables it. When enabled, the compiler
+1 -1
View File
@@ -7,7 +7,7 @@
use napi::Either;
use napi_derive::napi;
/// Options for the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
/// Options for the experimental [React Compiler](https://github.com/react/react/tree/main/compiler).
///
/// Mirrors the compiler's `PluginOptions`. The deep `environment` configuration
/// (inference / validation flags) is not surfaced here.
+2 -20
View File
@@ -149,7 +149,7 @@ pub struct TransformOptions {
/// Decorator plugin
pub decorator: Option<DecoratorOptions>,
/// Enable the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
/// Enable the experimental [React Compiler](https://github.com/react/react/tree/main/compiler).
///
/// `true` enables it with default options; an object enables it with the
/// given options; `false` or omitted disables it. When enabled, the compiler
@@ -202,20 +202,11 @@ impl TryFrom<TransformOptions> for oxc::transformer::TransformOptions {
.plugins
.map(oxc::transformer::PluginsOptions::from)
.unwrap_or_default(),
react_compiler: crate::react_compiler::resolve(options.react_compiler),
})
}
}
impl TransformOptions {
/// Take the `reactCompiler` option and resolve it into the compiler's
/// `PluginOptions`. The React Compiler is a standalone pass driven by the
/// [`CompilerInterface`], so it is not part of
/// `oxc::transformer::TransformOptions`.
fn take_react_compiler(&mut self) -> Option<oxc_react_compiler::PluginOptions> {
crate::react_compiler::resolve(self.react_compiler.take())
}
}
#[napi(object)]
#[derive(Default, Debug)]
pub struct CompilerAssumptions {
@@ -763,8 +754,6 @@ struct Compiler {
define: Option<ReplaceGlobalDefinesConfig>,
inject: Option<InjectGlobalVariablesConfig>,
#[expect(clippy::struct_field_names)]
react_compiler: Option<oxc_react_compiler::PluginOptions>,
helpers_used: FxHashMap<String, String>,
errors: Vec<OxcDiagnostic>,
@@ -817,8 +806,6 @@ impl Compiler {
.transpose()?
.map(InjectGlobalVariablesConfig::new);
let react_compiler = options.as_mut().and_then(TransformOptions::take_react_compiler);
let transform_options = match options {
Some(options) => oxc::transformer::TransformOptions::try_from(options)
.map_err(|err| vec![OxcDiagnostic::error(err)])?,
@@ -835,7 +822,6 @@ impl Compiler {
declaration_map: None,
define,
inject,
react_compiler,
helpers_used: FxHashMap::default(),
errors: vec![],
})
@@ -869,10 +855,6 @@ impl CompilerInterface for Compiler {
self.inject.clone()
}
fn react_compiler_options(&self) -> Option<oxc_react_compiler::PluginOptions> {
self.react_compiler.clone()
}
fn after_codegen(&mut self, ret: CodegenReturn) {
self.printed = ret.code;
self.printed_sourcemap = ret.map.map(SourceMap::from);
@@ -639,6 +639,18 @@ rebuilt : ScopeId(0): []
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/module-namespace/body-export-named-declaration/input.ts
Namespaces exporting non-const are not supported by Oxc. Change to const or see: https://oxc.rs/docs/guide/usage/transformer/typescript.html#partial-namespace-support
Symbol flags mismatch for "N":
after transform: SymbolId(0): SymbolFlags(ValueModule)
rebuilt : SymbolId(0): SymbolFlags(BlockScopedVariable)
Symbol span mismatch for "N":
after transform: SymbolId(0): Span { start: 10, end: 11 }
rebuilt : SymbolId(0): Span { start: 0, end: 0 }
Symbol flags mismatch for "X":
after transform: SymbolId(2): SymbolFlags(ValueModule)
rebuilt : SymbolId(3): SymbolFlags(BlockScopedVariable)
Symbol span mismatch for "X":
after transform: SymbolId(2): Span { start: 53, end: 54 }
rebuilt : SymbolId(3): Span { start: 0, end: 0 }
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/module-namespace/body-import-equals-internal/input.ts
Bindings mismatch:
@@ -661,6 +673,9 @@ rebuilt : ScopeId(0): []
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/module-namespace/head/input.ts
Ambient modules cannot be nested in other modules or namespaces.
Ambient modules cannot be nested in other modules or namespaces.
Bindings mismatch:
after transform: ScopeId(0): ["M", "N"]
rebuilt : ScopeId(0): []
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/module-namespace/head-declare/input.ts
Bindings mismatch:
@@ -783,6 +798,15 @@ rebuilt : ScopeId(0): []
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/scope/namespace-declaration-var-2/input.js
Namespaces exporting non-const are not supported by Oxc. Change to const or see: https://oxc.rs/docs/guide/usage/transformer/typescript.html#partial-namespace-support
Bindings mismatch:
after transform: ScopeId(0): ["bar", "foo"]
rebuilt : ScopeId(0): ["bar"]
Symbol flags mismatch for "bar":
after transform: SymbolId(1): SymbolFlags(ValueModule)
rebuilt : SymbolId(0): SymbolFlags(BlockScopedVariable)
Symbol span mismatch for "bar":
after transform: SymbolId(1): Span { start: 32, end: 35 }
rebuilt : SymbolId(0): Span { start: 0, end: 0 }
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/scope/redeclaration-class-interface/input.ts
Bindings mismatch:
File diff suppressed because it is too large. Load diff