feat(react_compiler): integrate the Rust port of the React Compiler (#22942)

Closes https://github.com/oxc-project/oxc/issues/10048

Integrates the Rust port of the React Compiler ([facebook/react#36173](https://github.com/facebook/react/pull/36173)) into oxc.

## Notes (resolved)

- Published crates can't reference Git URLs, so using this from Rolldown needs the React Compiler crates on crates.io. ✅ Published as a fork at https://crates.io/crates/forked_react_compiler; this PR depends on those `forked_react_compiler*` crates so people can get earlier access.
- The React Compiler crates had no license field. ✅ The published fork carries `license = "MIT"` (per the React repo's MIT license), so Cargo Deny and Security Analysis pass.

## Benchmark

**Wall-clock overhead vs plain transform** (release `transformSync`, same `jsx: automatic`, toggling only `reactCompiler`):

| fixture | without | with | overhead |
| --- | --- | --- | --- |
| RadixUIAdoptionSection.jsx (2.5 KiB) | 0.03 ms | 1.81 ms | +1.8 ms |
| excalidraw `App.tsx` (406 KiB) | 4.09 ms | 14.49 ms | +10.4 ms (3.5×) |

So roughly a fixed ~1.8 ms floor per file plus a per-size cost — about **3.5× the transform time** on a large real-world component.

## Binary size

Linking the React Compiler pulls the whole compiler pipeline (HIR, lowering, inference, SSA, optimization, reactive scopes, validation) plus the oxc⇄Babel AST conversion into the binding. Release build of the napi transform addon (`transform.darwin-arm64.node`, `--release`, stripped), darwin-arm64:

| build | size |
| --- | --- |
| baseline (`main`) | 3.51 MiB |
| with React Compiler | 8.66 MiB |
| **delta** | **+5.14 MiB (+146%, 2.46×)** |
This commit is contained in:
Boshen committed 2026-06-08 13:32:56 +00:00
1 parent 7d89909a3f
commit b846ab2d9a
23 files changed
+8786 -9

No files matched your search

@@ -21,6 +21,7 @@ const ALL_COMPONENTS = [
"codegen",
"formatter",
"pipeline",
"react_compiler",
"linter",
];
@@ -61,6 +62,9 @@ function getFeatureForComponent(component) {
if (component === "linter") {
return "linter";
}
if (component === "react_compiler") {
return "react_compiler";
}
return "compiler";
}
Generated
+199 -6
View File
@@ -571,6 +571,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common 0.1.6",
"subtle",
]
[[package]]
@@ -700,7 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -777,6 +778,159 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "forked_react_compiler"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34495a9926779b57310fd72e031fafdabc90f7c62e188aa5317ca08a115cce21"
dependencies = [
"forked_react_compiler_ast",
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"forked_react_compiler_inference",
"forked_react_compiler_lowering",
"forked_react_compiler_optimization",
"forked_react_compiler_reactive_scopes",
"forked_react_compiler_ssa",
"forked_react_compiler_typeinference",
"forked_react_compiler_validation",
"indexmap",
"regex-lite",
"serde",
"serde_json",
]
[[package]]
name = "forked_react_compiler_ast"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8ed273c27b68afe5b5f3bec2cd0215d6507f93921bf93a63b77f4362699c67e"
dependencies = [
"indexmap",
"serde",
"serde_json",
]
[[package]]
name = "forked_react_compiler_diagnostics"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0c5c845634ca8381a4dba94fc53f757c85de2e1e29711ef8fd7b5a4ca1fe9f"
dependencies = [
"serde",
]
[[package]]
name = "forked_react_compiler_hir"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bbec4ac046d8eac451b16c75d51d7709b10cbfdc8a52e7c580e1e45239491b"
dependencies = [
"forked_react_compiler_diagnostics",
"indexmap",
"serde",
"serde_json",
]
[[package]]
name = "forked_react_compiler_inference"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c910dd22822ab23c29d2e969f3e3ca9302fb10c065fcf48b83344dab715e798"
dependencies = [
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"forked_react_compiler_lowering",
"forked_react_compiler_optimization",
"forked_react_compiler_ssa",
"forked_react_compiler_utils",
"indexmap",
]
[[package]]
name = "forked_react_compiler_lowering"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d891d2ea5d43aa948c56a036960059f405489c60089e57c261be4f35f05d815"
dependencies = [
"forked_react_compiler_ast",
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"indexmap",
"serde_json",
]
[[package]]
name = "forked_react_compiler_optimization"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8611f21629cf6333c1b634e1bca816c863d4cba48d4ac1f42d61203835797de9"
dependencies = [
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"forked_react_compiler_lowering",
"forked_react_compiler_ssa",
"indexmap",
]
[[package]]
name = "forked_react_compiler_reactive_scopes"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "294a00391f2051dec90669fdff54397a464b16bb568117b48a959bed56d2798b"
dependencies = [
"forked_react_compiler_ast",
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"hmac",
"indexmap",
"serde_json",
"sha2",
]
[[package]]
name = "forked_react_compiler_ssa"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4782ac57df264c71abaabf68e90b9e3a96f3d607e772bc9ee6e9d26654614aa"
dependencies = [
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"forked_react_compiler_lowering",
"indexmap",
]
[[package]]
name = "forked_react_compiler_typeinference"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15d9379f0f48a565d82ed03de02946571af209057cfdc81fe09355518e587a54"
dependencies = [
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"forked_react_compiler_ssa",
]
[[package]]
name = "forked_react_compiler_utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531d2adbbfa861f9669f70e49eb1cc27e8817a7f1d000bee3e2c71a2d82217ec"
dependencies = [
"indexmap",
]
[[package]]
name = "forked_react_compiler_validation"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f8b2c12e00b41dc87bc2c63656b473047dcdf75b1f5dc545ca93a308d53a89"
dependencies = [
"forked_react_compiler_diagnostics",
"forked_react_compiler_hir",
"indexmap",
]
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -1014,6 +1168,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "http"
version = "1.3.1"
@@ -1554,7 +1717,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1653,6 +1816,7 @@ dependencies = [
"oxc_mangler",
"oxc_minifier",
"oxc_parser",
"oxc_react_compiler",
"oxc_regular_expression",
"oxc_semantic",
"oxc_span",
@@ -1826,6 +1990,7 @@ dependencies = [
"oxc_mangler",
"oxc_minifier",
"oxc_parser",
"oxc_react_compiler",
"oxc_semantic",
"oxc_span",
"oxc_tasks_common",
@@ -2358,6 +2523,27 @@ dependencies = [
"walkdir",
]
[[package]]
name = "oxc_react_compiler"
version = "0.134.0"
dependencies = [
"forked_react_compiler",
"forked_react_compiler_ast",
"forked_react_compiler_hir",
"indexmap",
"oxc_allocator",
"oxc_ast",
"oxc_ast_visit",
"oxc_codegen",
"oxc_diagnostics",
"oxc_parser",
"oxc_semantic",
"oxc_span",
"oxc_syntax",
"rustc-hash",
"serde_json",
]
[[package]]
name = "oxc_regular_expression"
version = "0.134.0"
@@ -2563,6 +2749,7 @@ dependencies = [
"napi-derive",
"oxc",
"oxc_napi",
"oxc_react_compiler",
"oxc_sourcemap",
"rustc-hash",
]
@@ -3042,6 +3229,12 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-lite"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
[[package]]
name = "regex-syntax"
version = "0.8.8"
@@ -3118,7 +3311,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3213,7 +3406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3508,7 +3701,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4037,7 +4230,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
+1
View File
@@ -131,6 +131,7 @@ oxc_parser = { version = "0.134.0", path = "crates/oxc_parser", features = [
"regular_expression",
] } # JS/TS parser
oxc_parser_napi = { version = "0.134.0", path = "napi/parser" } # Node.js parser binding
oxc_react_compiler = { version = "0.134.0", path = "crates/oxc_react_compiler" } # React Compiler integration (experimental)
oxc_regular_expression = { version = "0.134.0", path = "crates/oxc_regular_expression" } # Regex parser
oxc_semantic = { version = "0.134.0", path = "crates/oxc_semantic" } # Semantic analysis
oxc_span = { version = "0.134.0", path = "crates/oxc_span" } # Source positions
+2 -1
View File
@@ -36,6 +36,7 @@ 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 }
@@ -58,7 +59,7 @@ full = [
]
semantic = ["oxc_semantic"]
transformer = ["oxc_transformer", "oxc_transformer_plugins"]
transformer = ["oxc_transformer", "oxc_transformer_plugins", "oxc_react_compiler"]
minifier = ["oxc_mangler", "oxc_minifier"]
codegen = ["oxc_codegen", "oxc_codegen/sourcemap"]
mangler = ["oxc_mangler"]
+30 -1
View File
@@ -8,6 +8,7 @@ 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};
use oxc_span::SourceType;
use oxc_transformer::{TransformOptions, Transformer, TransformerReturn};
@@ -70,6 +71,13 @@ 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
}
@@ -148,6 +156,15 @@ 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() {
scoping =
oxc_react_compiler::run(&mut program, &allocator, scoping, &options, &mut errors);
}
/* Transform */
if let Some(options) = self.transform_options() {
@@ -155,15 +172,27 @@ pub trait CompilerInterface {
self.transform(options, &allocator, &mut program, source_path, scoping);
if !transformer_return.errors.is_empty() {
self.handle_errors(transformer_return.errors);
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);
}
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);
}
let inject_options = self.inject_options();
+66
View File
@@ -0,0 +1,66 @@
# oxc_react_compiler
#
# Native oxc integration for the Rust port of React Compiler (facebook/react#36173).
#
# 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
# against the live workspace oxc AST.
[package]
name = "oxc_react_compiler"
version = "0.134.0"
publish = true
authors.workspace = true
categories.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
description = "oxc integration for the Rust port of React Compiler"
[lib]
doctest = false
[dependencies]
oxc_allocator = { workspace = true }
oxc_ast = { workspace = true }
oxc_ast_visit = { workspace = true }
oxc_codegen = { workspace = true }
oxc_diagnostics = { workspace = true }
oxc_parser = { workspace = true }
oxc_semantic = { workspace = true }
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*`.
react_compiler = { package = "forked_react_compiler", version = "0.1.3" }
react_compiler_ast = { package = "forked_react_compiler_ast", version = "0.1.3" }
react_compiler_hir = { package = "forked_react_compiler_hir", version = "0.1.3" }
indexmap = { workspace = true, features = ["serde"] }
rustc-hash = { workspace = true }
serde_json = { workspace = true, features = ["raw_value"] }
# The `src/` conversion modules are vendored close to upstream `react_compiler_oxc`
# (kept near byte-for-byte to ease re-syncing), so relax the handful of lints they
# trip here rather than editing the code. Defining `[lints.clippy]` means this crate
# does not inherit `[lints] workspace = true`, which keeps the in-house restriction
# lints (`todo!`/`unimplemented!`/...) off the vendored code while clippy's default
# correctness and perf checks still apply.
[lints.clippy]
disallowed_types = "allow" # `react_compiler_ast`'s `ScopeInfo`/`ScopeData` fields are std `HashMap`
collapsible_if = "allow"
needless_return = "allow"
unnecessary_unwrap = "allow"
# The vendored modules' doc comments reference types like `Vec<PatternLike>`
# without backticks; relax rustdoc for them rather than editing upstream code.
[lints.rustdoc]
broken_intra_doc_links = "allow"
invalid_html_tags = "allow"
bare_urls = "allow"
+18
View File
@@ -0,0 +1,18 @@
# Oxc React Compiler
oxc integration for the Rust port of the [React Compiler](https://github.com/facebook/react/pull/36173).
## Overview
This crate owns the oxc &harr; `react_compiler_ast` (Babel) conversion layer and runs the React
Compiler over an oxc AST, memoizing React components and hooks. The compiler _core_ crates are
front-end agnostic (they never depend on oxc), so they are consumed from crates.io as a published
fork; the AST and scope conversion lives here, written against the live oxc AST.
## API
- `transform` — run the compiler and return the compiled oxc `Program` plus diagnostics.
- `run` — standalone pass that rewrites a program in place and returns the scoping the rest of the
pipeline should use.
- `lint` — report diagnostics only, without emitting code.
- `default_plugin_options` / `PluginOptions` — configure which functions are compiled and how.
@@ -0,0 +1,63 @@
//! # 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.
//!
//! ## Usage
//!
//! ```bash
//! just example react_compiler MyFile.jsx
//! ```
//!
//! [facebook/react#36173]: https://github.com/facebook/react/pull/36173
use std::path::Path;
use oxc_allocator::Allocator;
use oxc_codegen::Codegen;
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_react_compiler::{default_plugin_options, transform};
/// Compile a React component with the Rust React Compiler and print the result.
fn main() {
let name = std::env::args().nth(1).unwrap_or_else(|| {
eprintln!("usage: cargo run -p oxc_react_compiler --example react_compiler -- <FILE>");
std::process::exit(1);
});
let path = Path::new(&name);
let source_text =
std::fs::read_to_string(path).unwrap_or_else(|err| panic!("{name} not found.\n{err}"));
let source_type = SourceType::from_path(path).unwrap_or_else(|_| SourceType::tsx());
println!("Original ({name}):\n");
println!("{source_text}");
let allocator = Allocator::default();
let program = Parser::new(&allocator, &source_text, source_type).parse().program;
let semantic = SemanticBuilder::new().build(&program).semantic;
let result = transform(&program, &semantic, &allocator, default_plugin_options());
if !result.diagnostics.is_empty() {
println!("Diagnostics:\n");
for diagnostic in &result.diagnostics {
println!("{diagnostic:?}");
}
println!();
}
match result.program {
Some(compiled) => {
let output = Codegen::new().build(&compiled).code;
println!("Compiled:\n");
println!("{output}");
}
None => {
println!("No changes: no React component or hook found (or compilation bailed out).");
}
}
}
@@ -0,0 +1,158 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
//! Apply the React Compiler's binding renames to identifiers in uncompiled
//! sibling code (compiled functions already carry the renamed identifiers).
use oxc_ast::ast::*;
use oxc_ast_visit::VisitMut;
use oxc_ast_visit::walk_mut;
use react_compiler::entrypoint::compile_result::BindingRenameInfo;
use react_compiler_ast::scope::BindingId;
use react_compiler_ast::scope::ScopeInfo;
use rustc_hash::FxHashMap;
/// Map each `span.start` of a renamed-binding reference to its new name.
pub fn build_rename_plan(
scope_info: &ScopeInfo,
renames: &[BindingRenameInfo],
) -> FxHashMap<u32, String> {
if renames.is_empty() {
return FxHashMap::default();
}
let renames_by_declaration: FxHashMap<u32, &BindingRenameInfo> =
renames.iter().map(|rename| (rename.declaration_start, rename)).collect();
let mut renamed_bindings: FxHashMap<BindingId, String> = FxHashMap::default();
for binding in &scope_info.bindings {
let Some(rename) =
binding.declaration_start.and_then(|start| renames_by_declaration.get(&start))
else {
continue;
};
if binding.name == rename.original {
renamed_bindings.insert(binding.id, rename.renamed.clone());
}
}
if renamed_bindings.is_empty() {
return FxHashMap::default();
}
// `ref_node_id_to_binding` is node-id keyed; in OXC node_id == span.start.
scope_info
.ref_node_id_to_binding
.iter()
.filter_map(|(&position, binding_id)| {
if scope_info.bindings[binding_id.0 as usize].declaration_start == Some(position) {
return None;
}
renamed_bindings.get(binding_id).map(|renamed| (position, renamed.clone()))
})
.collect()
}
/// Walk the program and apply `rename_plan` (keyed by `span.start`), expanding
/// shorthand properties `{x}` → `{x: x_0}` when the value is renamed.
pub fn apply_renames<'a>(
program: &mut Program<'a>,
rename_plan: &FxHashMap<u32, String>,
allocator: &'a oxc_allocator::Allocator,
) {
if rename_plan.is_empty() {
return;
}
walk_mut::walk_program(&mut RenameApplyVisitor { rename_plan, allocator }, program);
}
struct RenameApplyVisitor<'a, 'p> {
rename_plan: &'p FxHashMap<u32, String>,
allocator: &'a oxc_allocator::Allocator,
}
impl<'a, 'p> RenameApplyVisitor<'a, 'p> {
fn renamed_at(&self, position: u32) -> Option<&str> {
self.rename_plan.get(&position).map(|s| s.as_str())
}
}
impl<'a> VisitMut<'a> for RenameApplyVisitor<'a, '_> {
fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) {
if let Some(renamed) = self.renamed_at(ident.span.start) {
ident.name = oxc_allocator::StringBuilder::from_str_in(renamed, self.allocator)
.into_str()
.into();
}
}
fn visit_binding_identifier(&mut self, ident: &mut BindingIdentifier<'a>) {
if let Some(renamed) = self.renamed_at(ident.span.start) {
ident.name = oxc_allocator::StringBuilder::from_str_in(renamed, self.allocator)
.into_str()
.into();
}
}
/// Rename the object of a member expression, never a static property name.
fn visit_member_expression(&mut self, expr: &mut MemberExpression<'a>) {
match expr {
MemberExpression::StaticMemberExpression(static_expr) => {
self.visit_expression(&mut static_expr.object);
}
MemberExpression::ComputedMemberExpression(computed_expr) => {
self.visit_expression(&mut computed_expr.object);
self.visit_expression(&mut computed_expr.expression);
}
MemberExpression::PrivateFieldExpression(private_expr) => {
self.visit_expression(&mut private_expr.object);
}
}
}
/// Expand a renamed shorthand object property `{x}` into `{x: x_0}`.
fn visit_object_property(&mut self, prop: &mut ObjectProperty<'a>) {
if prop.shorthand {
if let Expression::Identifier(ref ident) = prop.value {
if let Some(renamed) = self.renamed_at(ident.span.start) {
prop.shorthand = false;
if let Expression::Identifier(ref mut ident) = prop.value {
ident.name =
oxc_allocator::StringBuilder::from_str_in(renamed, self.allocator)
.into_str()
.into();
}
return;
}
}
}
if prop.computed {
self.visit_property_key(&mut prop.key);
}
self.visit_expression(&mut prop.value);
}
/// Expand a renamed shorthand binding property `{x}` into `{x: x_0}`.
fn visit_binding_property(&mut self, prop: &mut BindingProperty<'a>) {
if prop.shorthand {
if let BindingPattern::BindingIdentifier(ref ident) = prop.value {
if let Some(renamed) = self.renamed_at(ident.span.start) {
prop.shorthand = false;
if let BindingPattern::BindingIdentifier(ref mut ident) = prop.value {
ident.name =
oxc_allocator::StringBuilder::from_str_in(renamed, self.allocator)
.into_str()
.into();
}
return;
}
}
}
if prop.computed {
self.visit_property_key(&mut prop.key);
}
self.visit_binding_pattern(&mut prop.value);
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,519 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
use std::collections::HashMap;
use indexmap::IndexMap;
use oxc_ast::AstKind;
use oxc_ast::ast::Program;
use oxc_semantic::Semantic;
use oxc_span::GetSpan;
use oxc_syntax::symbol::SymbolFlags;
use react_compiler_ast::scope::*;
use rustc_hash::FxHashMap;
/// Convert OXC's semantic analysis into React Compiler's ScopeInfo.
pub fn convert_scope_info(semantic: &Semantic, _program: &Program) -> ScopeInfo {
let scoping = semantic.scoping();
let nodes = semantic.nodes();
let mut scopes: Vec<ScopeData> = Vec::new();
let mut bindings: Vec<BindingData> = Vec::new();
let mut node_to_scope: HashMap<u32, ScopeId> = HashMap::new();
let mut node_to_scope_end: HashMap<u32, u32> = HashMap::new();
// In OXC, span.start is used as node_id (OXC spans are unique).
let mut node_id_to_scope: HashMap<u32, ScopeId> = HashMap::new();
let mut ref_node_id_to_binding: IndexMap<u32, BindingId> = IndexMap::new();
let mut symbol_to_binding: FxHashMap<oxc_syntax::symbol::SymbolId, BindingId> =
FxHashMap::default();
// First pass: Create all bindings from symbols
for symbol_id in scoping.symbol_ids() {
let symbol_flags = scoping.symbol_flags(symbol_id);
let name = scoping.symbol_name(symbol_id).to_string();
let kind = get_binding_kind(symbol_flags, semantic, symbol_id);
let (declaration_type, declaration_start) =
get_declaration_info(semantic, symbol_id, &name);
let import = if matches!(kind, BindingKind::Module) {
get_import_data(semantic, symbol_id)
} else {
None
};
let binding_id = BindingId(bindings.len() as u32);
symbol_to_binding.insert(symbol_id, binding_id);
bindings.push(BindingData {
id: binding_id,
name,
kind,
scope: ScopeId(0), // Placeholder, filled in second pass
declaration_type,
declaration_start,
declaration_node_id: declaration_start,
import,
});
}
// Second pass: Create all scopes and update binding scope references
for scope_id in scoping.scope_descendants_from_root() {
let scope_flags = scoping.scope_flags(scope_id);
let parent = scoping.scope_parent_id(scope_id);
let our_scope_id = ScopeId(scope_id.index() as u32);
let kind = get_scope_kind(scope_flags, semantic, scope_id);
let mut scope_bindings: HashMap<String, BindingId> = HashMap::new();
for symbol_id in scoping.iter_bindings_in(scope_id) {
if let Some(&binding_id) = symbol_to_binding.get(&symbol_id) {
let name = bindings[binding_id.0 as usize].name.clone();
scope_bindings.insert(name, binding_id);
bindings[binding_id.0 as usize].scope = our_scope_id;
}
}
let node_id = scoping.get_node_id(scope_id);
let node = nodes.get_node(node_id);
let span = node.kind().span();
let start = span.start;
let end = span.end;
// For function scopes inside object methods, also map the parent
// ObjectProperty start so the compiler can look up the scope using the
// ObjectMethod's start position (which matches the property start in Babel).
if matches!(kind, ScopeKind::Function) {
if let AstKind::Function(_) = node.kind() {
let parent_node_id = nodes.parent_id(node_id);
let parent_node = nodes.get_node(parent_node_id);
match parent_node.kind() {
AstKind::ObjectProperty(prop)
if prop.method
|| matches!(
prop.kind,
oxc_ast::ast::PropertyKind::Get | oxc_ast::ast::PropertyKind::Set
) =>
{
let prop_start = parent_node.kind().span().start;
if prop_start != start {
node_to_scope.insert(prop_start, our_scope_id);
node_to_scope_end.insert(prop_start, end);
node_id_to_scope.insert(prop_start, our_scope_id);
}
}
_ => {}
}
}
}
if end > start {
node_to_scope.insert(start, our_scope_id);
node_to_scope_end.insert(start, end);
} else {
node_to_scope.insert(start, our_scope_id);
}
node_id_to_scope.insert(start, our_scope_id);
scopes.push(ScopeData {
id: our_scope_id,
parent: parent.map(|p| ScopeId(p.index() as u32)),
kind,
bindings: scope_bindings,
});
}
// Third pass: Map all resolved references to bindings
for symbol_id in scoping.symbol_ids() {
if let Some(&binding_id) = symbol_to_binding.get(&symbol_id) {
for &ref_id in scoping.get_resolved_reference_ids(symbol_id) {
let reference = scoping.get_reference(ref_id);
let ref_node = nodes.get_node(reference.node_id());
let start = ref_node.kind().span().start;
ref_node_id_to_binding.insert(start, binding_id);
}
}
}
// Also map declaration identifiers to their bindings
for symbol_id in scoping.symbol_ids() {
if let Some(&binding_id) = symbol_to_binding.get(&symbol_id) {
if let Some(start) = bindings[binding_id.0 as usize].declaration_start {
ref_node_id_to_binding.entry(start).or_insert(binding_id);
}
}
}
// Map `export default function Foo` — Babel treats the ExportDefaultDeclaration
// as a reference to the function's binding. OXC doesn't create a reference for
// the export itself, so we add one at the export statement's start position.
for stmt in &_program.body {
if let oxc_ast::ast::Statement::ExportDefaultDeclaration(export) = stmt {
if let oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(func) =
&export.declaration
{
if let Some(id) = &func.id {
let name = id.name.as_str();
if let Some(symbol_id) = id.symbol_id.get() {
if let Some(&binding_id) = symbol_to_binding.get(&symbol_id) {
let export_start = export.span().start;
ref_node_id_to_binding.entry(export_start).or_insert(binding_id);
}
} else {
// Fallback: look up binding by name
for (sym_id, &bind_id) in &symbol_to_binding {
if scoping.symbol_name(*sym_id) == name {
let export_start = export.span().start;
ref_node_id_to_binding.entry(export_start).or_insert(bind_id);
break;
}
}
}
}
}
}
}
// Map `export default function Foo` — Babel treats the ExportDefaultDeclaration
// as a reference to the function's binding. OXC doesn't create a reference for
// the export itself, so we add one at the export statement's start position.
for stmt in &_program.body {
if let oxc_ast::ast::Statement::ExportDefaultDeclaration(export) = stmt {
if let oxc_ast::ast::ExportDefaultDeclarationKind::FunctionDeclaration(func) =
&export.declaration
{
if let Some(id) = &func.id {
let name = id.name.as_str();
if let Some(symbol_id) = id.symbol_id.get() {
if let Some(&binding_id) = symbol_to_binding.get(&symbol_id) {
ref_node_id_to_binding.entry(export.span().start).or_insert(binding_id);
}
} else {
// Fallback: look up binding by name
for (sym_id, &bind_id) in &symbol_to_binding {
if scoping.symbol_name(*sym_id) == name {
ref_node_id_to_binding
.entry(export.span().start)
.or_insert(bind_id);
break;
}
}
}
}
}
}
}
let program_scope = ScopeId(scoping.root_scope_id().index() as u32);
ScopeInfo {
scopes,
bindings,
node_to_scope,
node_to_scope_end,
reference_to_binding: IndexMap::new(),
ref_node_id_to_binding,
node_id_to_scope,
program_scope,
}
}
/// Map OXC ScopeFlags to our ScopeKind.
fn get_scope_kind(
flags: oxc_syntax::scope::ScopeFlags,
semantic: &Semantic,
scope_id: oxc_syntax::scope::ScopeId,
) -> ScopeKind {
if flags.contains(oxc_syntax::scope::ScopeFlags::Top) {
return ScopeKind::Program;
}
if flags.intersects(oxc_syntax::scope::ScopeFlags::Function) {
return ScopeKind::Function;
}
if flags.contains(oxc_syntax::scope::ScopeFlags::CatchClause) {
return ScopeKind::Catch;
}
if flags.contains(oxc_syntax::scope::ScopeFlags::ClassStaticBlock) {
return ScopeKind::Class;
}
// Check the AST node to determine if it's a for loop, class, or switch
let node_id = semantic.scoping().get_node_id(scope_id);
let node = semantic.nodes().get_node(node_id);
match node.kind() {
AstKind::ForStatement(_) | AstKind::ForInStatement(_) | AstKind::ForOfStatement(_) => {
ScopeKind::For
}
AstKind::Class(_) => ScopeKind::Class,
AstKind::SwitchStatement(_) => ScopeKind::Switch,
_ => ScopeKind::Block,
}
}
/// Map OXC SymbolFlags to our BindingKind.
fn get_binding_kind(
flags: SymbolFlags,
semantic: &Semantic,
symbol_id: oxc_syntax::symbol::SymbolId,
) -> BindingKind {
if flags.contains(SymbolFlags::Import) {
return BindingKind::Module;
}
// Check the declaration node first — FormalParameter and CatchParameter
// need to be detected before the FunctionScopedVariable flag check, because
// OXC marks function parameters and catch parameters with FunctionScopedVariable.
let decl_node = semantic.symbol_declaration(symbol_id);
match decl_node.kind() {
AstKind::FormalParameter(_) => return BindingKind::Param,
AstKind::FormalParameterRest(_) => return BindingKind::Param,
AstKind::CatchParameter(_) => return BindingKind::Let,
AstKind::TSTypeAliasDeclaration(_) => return BindingKind::Local,
AstKind::TSEnumDeclaration(_) => return BindingKind::Local,
AstKind::TSModuleDeclaration(_) => return BindingKind::Local,
AstKind::Function(_) => {
if flags.contains(SymbolFlags::Function) {
return BindingKind::Hoisted;
}
return BindingKind::Local;
}
AstKind::Class(_) => return BindingKind::Local,
_ => {}
}
if flags.contains(SymbolFlags::FunctionScopedVariable) {
return BindingKind::Var;
}
if flags.contains(SymbolFlags::BlockScopedVariable) {
if flags.contains(SymbolFlags::ConstVariable) {
return BindingKind::Const;
} else {
return BindingKind::Let;
}
}
if flags.contains(SymbolFlags::Function) {
BindingKind::Hoisted
} else if flags.contains(SymbolFlags::Class) {
BindingKind::Local
} else {
BindingKind::Unknown
}
}
/// Get the declaration type string and start position for a binding.
fn get_declaration_info(
semantic: &Semantic,
symbol_id: oxc_syntax::symbol::SymbolId,
name: &str,
) -> (String, Option<u32>) {
let decl_node = semantic.symbol_declaration(symbol_id);
let declaration_type = ast_kind_to_string(decl_node.kind());
let declaration_start = find_binding_identifier_start(decl_node.kind(), name);
(declaration_type, declaration_start)
}
/// Convert an AstKind to its Babel-equivalent string representation.
fn ast_kind_to_string(kind: AstKind) -> String {
match kind {
AstKind::BindingIdentifier(_) => "BindingIdentifier",
AstKind::VariableDeclarator(_) => "VariableDeclarator",
AstKind::Function(f) => {
if f.is_declaration() {
"FunctionDeclaration"
} else {
"FunctionExpression"
}
}
AstKind::Class(c) => {
if c.is_declaration() {
"ClassDeclaration"
} else {
"ClassExpression"
}
}
AstKind::FormalParameter(_) => "FormalParameter",
AstKind::FormalParameterRest(_) => "FormalParameter",
AstKind::ImportSpecifier(_) => "ImportSpecifier",
AstKind::ImportDefaultSpecifier(_) => "ImportDefaultSpecifier",
AstKind::ImportNamespaceSpecifier(_) => "ImportNamespaceSpecifier",
AstKind::CatchClause(_) => "CatchClause",
AstKind::CatchParameter(_) => "CatchClause",
AstKind::TSTypeAliasDeclaration(_) => "TSTypeAliasDeclaration",
AstKind::TSEnumDeclaration(_) => "TSEnumDeclaration",
AstKind::TSModuleDeclaration(_) => "TSModuleDeclaration",
_ => "Unknown",
}
.to_string()
}
/// Find the binding identifier's start position within an AST node.
fn find_binding_identifier_start(kind: AstKind, name: &str) -> Option<u32> {
match kind {
AstKind::BindingIdentifier(ident) => {
if ident.name.as_str() == name {
Some(ident.span.start)
} else {
None
}
}
AstKind::VariableDeclarator(decl) => find_identifier_in_pattern(&decl.id, name),
AstKind::Function(func) => func
.id
.as_ref()
.and_then(|id| if id.name.as_str() == name { Some(id.span.start) } else { None }),
AstKind::Class(class) => class
.id
.as_ref()
.and_then(|id| if id.name.as_str() == name { Some(id.span.start) } else { None }),
AstKind::FormalParameter(param) => find_identifier_in_pattern(&param.pattern, name),
AstKind::FormalParameterRest(rest) => find_identifier_in_pattern(&rest.rest.argument, name),
AstKind::ImportSpecifier(spec) => Some(spec.local.span.start),
AstKind::ImportDefaultSpecifier(spec) => Some(spec.local.span.start),
AstKind::ImportNamespaceSpecifier(spec) => Some(spec.local.span.start),
AstKind::CatchClause(catch) => {
catch.param.as_ref().and_then(|p| find_identifier_in_pattern(&p.pattern, name))
}
AstKind::CatchParameter(param) => find_identifier_in_pattern(&param.pattern, name),
AstKind::TSTypeAliasDeclaration(decl) => {
if decl.id.name.as_str() == name {
Some(decl.id.span.start)
} else {
None
}
}
AstKind::TSEnumDeclaration(decl) => {
if decl.id.name.as_str() == name {
Some(decl.id.span.start)
} else {
None
}
}
AstKind::TSModuleDeclaration(decl) => {
match &decl.id {
oxc_ast::ast::TSModuleDeclarationName::Identifier(id) => {
if id.name.as_str() == name { Some(id.span.start) } else { None }
}
_ => None,
}
}
_ => None,
}
}
/// Recursively find a binding identifier within a binding pattern.
fn find_identifier_in_pattern(pattern: &oxc_ast::ast::BindingPattern, name: &str) -> Option<u32> {
use oxc_ast::ast::BindingPattern;
match pattern {
BindingPattern::BindingIdentifier(ident) => {
if ident.name.as_str() == name {
Some(ident.span.start)
} else {
None
}
}
BindingPattern::ObjectPattern(obj) => {
for prop in &obj.properties {
if let Some(start) = find_identifier_in_pattern(&prop.value, name) {
return Some(start);
}
}
if let Some(rest) = &obj.rest {
if let Some(start) = find_identifier_in_pattern(&rest.argument, name) {
return Some(start);
}
}
None
}
BindingPattern::ArrayPattern(arr) => {
for element in arr.elements.iter().flatten() {
if let Some(start) = find_identifier_in_pattern(element, name) {
return Some(start);
}
}
if let Some(rest) = &arr.rest {
if let Some(start) = find_identifier_in_pattern(&rest.argument, name) {
return Some(start);
}
}
None
}
BindingPattern::AssignmentPattern(assign) => find_identifier_in_pattern(&assign.left, name),
}
}
/// Extract import data for a module binding.
fn get_import_data(
semantic: &Semantic,
symbol_id: oxc_syntax::symbol::SymbolId,
) -> Option<ImportBindingData> {
let decl_node = semantic.symbol_declaration(symbol_id);
match decl_node.kind() {
AstKind::ImportDefaultSpecifier(_) => {
let import_decl = find_import_declaration(semantic, decl_node.id())?;
Some(ImportBindingData {
source: import_decl.source.value.to_string(),
kind: ImportBindingKind::Default,
imported: None,
})
}
AstKind::ImportNamespaceSpecifier(_) => {
let import_decl = find_import_declaration(semantic, decl_node.id())?;
Some(ImportBindingData {
source: import_decl.source.value.to_string(),
kind: ImportBindingKind::Namespace,
imported: None,
})
}
AstKind::ImportSpecifier(spec) => {
let import_decl = find_import_declaration(semantic, decl_node.id())?;
let imported_name = match &spec.imported {
oxc_ast::ast::ModuleExportName::IdentifierName(ident) => ident.name.to_string(),
oxc_ast::ast::ModuleExportName::IdentifierReference(ident) => {
ident.name.to_string()
}
oxc_ast::ast::ModuleExportName::StringLiteral(lit) => lit.value.to_string(),
};
Some(ImportBindingData {
source: import_decl.source.value.to_string(),
kind: ImportBindingKind::Named,
imported: Some(imported_name),
})
}
_ => None,
}
}
/// Find the ImportDeclaration node that contains the given import specifier.
fn find_import_declaration<'a>(
semantic: &'a Semantic,
specifier_node_id: oxc_semantic::NodeId,
) -> Option<&'a oxc_ast::ast::ImportDeclaration<'a>> {
let mut current_id = specifier_node_id;
// Walk up the parent chain (max 10 levels to avoid infinite loop)
for _ in 0..10 {
let parent_id = semantic.nodes().parent_id(current_id);
if parent_id == current_id {
// Root node, no more parents
return None;
}
let parent_node = semantic.nodes().get_node(parent_id);
if let AstKind::ImportDeclaration(decl) = parent_node.kind() {
return Some(decl);
}
current_id = parent_id;
}
None
}
@@ -0,0 +1,80 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
use oxc_diagnostics::OxcDiagnostic;
use react_compiler::entrypoint::compile_result::{
CompileResult, CompilerErrorDetailInfo, LoggerEvent,
};
/// Convert a `CompileResult` into OXC diagnostics.
pub fn compile_result_to_diagnostics(result: &CompileResult) -> Vec<OxcDiagnostic> {
let mut diagnostics = Vec::new();
match result {
CompileResult::Success { events, .. } => {
for event in events {
if let Some(diag) = event_to_diagnostic(event) {
diagnostics.push(diag);
}
}
}
CompileResult::Error { error, events, .. } => {
diagnostics.push(error_info_to_diagnostic(error));
for event in events {
if let Some(diag) = event_to_diagnostic(event) {
diagnostics.push(diag);
}
}
}
}
diagnostics
}
fn error_info_to_diagnostic(
error: &react_compiler::entrypoint::compile_result::CompilerErrorInfo,
) -> OxcDiagnostic {
let message = format!("[ReactCompiler] {}", error.reason);
let mut diag = OxcDiagnostic::error(message);
if let Some(description) = &error.description {
diag = diag.with_help(description.clone());
}
diag
}
/// Map a detail to an [`OxcDiagnostic`] at the compiler's own *display* severity
/// (`Error`/`Warning`/`Hint`; `Off` is suppressed). Fatality is separate, decided
/// by `panicThreshold` ([`CompileResult::Error`]).
fn error_detail_to_diagnostic(detail: &CompilerErrorDetailInfo) -> Option<OxcDiagnostic> {
let message = if let Some(description) = &detail.description {
format!("[ReactCompiler] {}: {}. {}", detail.category, detail.reason, description)
} else {
format!("[ReactCompiler] {}: {}", detail.category, detail.reason)
};
let diagnostic = match detail.severity.as_str() {
"Off" => return None,
"Error" => OxcDiagnostic::error(message),
// `Warning`, `Hint`, and any unknown future value surface as warnings.
_ => OxcDiagnostic::warn(message),
};
Some(diagnostic)
}
fn event_to_diagnostic(event: &LoggerEvent) -> Option<OxcDiagnostic> {
match event {
LoggerEvent::CompileSuccess { .. } | LoggerEvent::CompileSkip { .. } => None,
LoggerEvent::CompileError { detail, .. }
| LoggerEvent::CompileErrorWithLoc { detail, .. } => error_detail_to_diagnostic(detail),
LoggerEvent::CompileUnexpectedThrow { data, .. } => {
Some(OxcDiagnostic::error(format!("[ReactCompiler] Unexpected error: {}", data)))
}
LoggerEvent::PipelineError { data, .. } => {
Some(OxcDiagnostic::error(format!("[ReactCompiler] Pipeline error: {}", data)))
}
}
}
+638
View File
@@ -0,0 +1,638 @@
pub mod apply_renames;
pub mod convert_ast;
pub mod convert_ast_reverse;
pub mod convert_scope;
pub mod diagnostics;
pub mod prefilter;
use apply_renames::build_rename_plan;
use convert_ast::convert_program;
use convert_scope::convert_scope_info;
use diagnostics::compile_result_to_diagnostics;
use prefilter::{has_react_like_functions, has_resource_management_declarations};
use react_compiler::entrypoint::compile_result::LoggerEvent;
use react_compiler_hir::environment_config::EnvironmentConfig;
use rustc_hash::FxHashSet;
// Re-exported so integrations needn't depend on the upstream `react_compiler` crate.
pub use react_compiler::entrypoint::plugin_options::{
CompilerTarget, DynamicGatingConfig, GatingConfig, PluginOptions,
};
/// [`PluginOptions`] with the compiler's standard defaults (it has no `Default`).
/// Override fields with struct-update syntax: `PluginOptions { ..default_plugin_options() }`.
pub fn default_plugin_options() -> PluginOptions {
PluginOptions {
should_compile: true,
enable_reanimated: false,
is_dev: false,
filename: None,
compilation_mode: "infer".to_string(),
panic_threshold: "none".to_string(),
target: CompilerTarget::Version("19".to_string()),
gating: None,
dynamic_gating: None,
no_emit: false,
output_mode: None,
eslint_suppression_rules: None,
flow_suppressions: true,
ignore_use_no_forget: false,
custom_opt_out_directives: None,
environment: EnvironmentConfig::default(),
source_code: None,
profiling: false,
debug: false,
}
}
pub struct TransformResult<'a> {
/// Compiled, ready-to-codegen OXC AST; `None` if the compiler made no changes.
pub program: Option<oxc_ast::ast::Program<'a>>,
pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
pub events: Vec<LoggerEvent>,
}
pub struct LintResult {
pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
}
/// Transform a pre-parsed program. `program` is `None` when nothing was compiled.
pub fn transform<'a>(
program: &oxc_ast::ast::Program<'a>,
semantic: &oxc_semantic::Semantic,
allocator: &'a oxc_allocator::Allocator,
options: PluginOptions,
) -> TransformResult<'a> {
let source_text = program.source_text;
// Skip files with no React-like functions, unless the mode compiles everything.
if !matches!(options.compilation_mode.as_str(), "all" | "annotation")
&& !has_react_like_functions(program)
{
return TransformResult { program: None, diagnostics: vec![], events: vec![] };
}
// `using`/`await using` disposal semantics aren't preserved yet — skip the file.
if has_resource_management_declarations(program) {
return TransformResult { program: None, diagnostics: vec![], events: vec![] };
}
let file = convert_program(program, source_text);
let scope_info = convert_scope_info(semantic, program);
let result =
react_compiler::entrypoint::program::compile_program(file, scope_info.clone(), options);
let diagnostics = compile_result_to_diagnostics(&result);
let (program_ast, events, renames) = match result {
react_compiler::entrypoint::compile_result::CompileResult::Success {
ast,
events,
renames,
..
} => (ast, events, renames),
react_compiler::entrypoint::compile_result::CompileResult::Error { events, .. } => {
(None, events, Vec::new())
}
};
// Rename plan maps source positions of uncompiled references to new names.
let rename_plan = build_rename_plan(&scope_info, &renames);
let compiled_program = program_ast.map(|file: react_compiler_ast::File| {
let mut compiled =
convert_ast_reverse::convert_program_to_oxc_with_source(&file, allocator, source_text);
compiled.source_type = program.source_type;
apply_renames::apply_renames(&mut compiled, &rename_plan, allocator);
preserve_comments(&mut compiled, source_text, allocator);
compiled
});
TransformResult { program: compiled_program, diagnostics, events }
}
/// Re-parse the source for comments and keep those attached to top-level
/// statements of the compiled program, so codegen can re-emit them.
fn preserve_comments<'a>(
program: &mut oxc_ast::ast::Program<'a>,
source_text: &str,
allocator: &'a oxc_allocator::Allocator,
) {
let comment_allocator = oxc_allocator::Allocator::default();
let source_type = oxc_span::SourceType::tsx();
let parsed = oxc_parser::Parser::new(&comment_allocator, source_text, source_type).parse();
// Keep only comments attached to a top-level statement; inner comments have
// `attached_to` positions that match no top-level statement.
let mut top_level_starts = FxHashSet::default();
top_level_starts.insert(0u32);
for stmt in &program.body {
use oxc_span::GetSpan;
let start = stmt.span().start;
if start > 0 {
top_level_starts.insert(start);
}
}
// Copy only comments attached to top-level statements.
let mut comments =
oxc_allocator::Vec::with_capacity_in(parsed.program.comments.len(), allocator);
for comment in &parsed.program.comments {
if top_level_starts.contains(&comment.attached_to) {
comments.push(*comment);
}
}
program.comments = comments;
// Copy the source into `allocator` so codegen can read comment content from spans.
let source_in_alloc = oxc_allocator::StringBuilder::from_str_in(source_text, allocator);
program.source_text = source_in_alloc.into_str();
}
/// Convenience wrapper — parses source text, runs semantic analysis, then transforms.
pub fn transform_source<'a>(
source_text: &'a str,
source_type: oxc_span::SourceType,
allocator: &'a oxc_allocator::Allocator,
options: PluginOptions,
) -> TransformResult<'a> {
let parsed = oxc_parser::Parser::new(allocator, source_text, source_type).parse();
let semantic =
oxc_semantic::SemanticBuilder::new().with_enum_eval(true).build(&parsed.program).semantic;
transform(&parsed.program, &semantic, allocator, options)
}
/// Lint a pre-parsed program — like [`transform`] but only collects diagnostics.
pub fn lint(
program: &oxc_ast::ast::Program,
semantic: &oxc_semantic::Semantic,
options: PluginOptions,
) -> LintResult {
let mut opts = options;
opts.no_emit = true;
// `no_emit` yields `program: None`; a local arena for the conversion suffices.
let allocator = oxc_allocator::Allocator::default();
let result = transform(program, semantic, &allocator, opts);
LintResult { diagnostics: result.diagnostics }
}
/// Convenience wrapper — parses source text, runs semantic analysis, then lints.
pub fn lint_source(
source_text: &str,
source_type: oxc_span::SourceType,
options: PluginOptions,
) -> LintResult {
let allocator = oxc_allocator::Allocator::default();
let parsed = oxc_parser::Parser::new(&allocator, source_text, source_type).parse();
let semantic =
oxc_semantic::SemanticBuilder::new().with_enum_eval(true).build(&parsed.program).semantic;
lint(&parsed.program, &semantic, options)
}
/// Run the React Compiler as a standalone pass, returning the `Scoping` for the
/// rest of the pipeline (rebuilt if the program changed). Must run **first**, on
/// the pristine AST, before any other transform.
pub fn run<'a>(
program: &mut oxc_ast::ast::Program<'a>,
allocator: &'a oxc_allocator::Allocator,
scoping: oxc_semantic::Scoping,
options: &PluginOptions,
errors: &mut std::vec::Vec<oxc_diagnostics::OxcDiagnostic>,
) -> oxc_semantic::Scoping {
// `compiled` lives in `allocator`, not borrowed from `*program`, so the
// reassignment below is sound.
let result = {
let semantic =
oxc_semantic::SemanticBuilder::new().with_enum_eval(true).build(program).semantic;
transform(program, &semantic, allocator, options.clone())
};
errors.extend(result.diagnostics);
let Some(compiled) = result.program else {
return scoping;
};
*program = compiled;
// Rebuild scoping for downstream transforms.
oxc_semantic::SemanticBuilder::new().with_enum_eval(true).build(program).semantic.into_scoping()
}
// End-to-end smoke tests: oxc parse + semantic -> convert -> compile -> convert
// back -> codegen.
#[cfg(test)]
mod tests {
use react_compiler::entrypoint::plugin_options::PluginOptions;
use super::transform_source;
fn options() -> PluginOptions {
// Only the non-`#[serde(default)]` fields are required; the rest default.
serde_json::from_value(serde_json::json!({
"shouldCompile": true,
"enableReanimated": false,
"isDev": false,
"filename": "Component.jsx",
}))
.unwrap()
}
#[test]
fn memoizes_a_component_end_to_end() {
let source = "function Component(props) {\n \
return <div onClick={() => props.onClick()}>{props.text}</div>;\n}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
assert!(result.diagnostics.is_empty(), "unexpected diagnostics: {:?}", result.diagnostics);
let program = result.program.expect("React Compiler should have transformed the component");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(
output.contains("react/compiler-runtime"),
"expected the compiler-runtime cache import in output:\n{output}"
);
assert!(
output.contains("_c("),
"expected memo cache reads (`_c(...)`) in output:\n{output}"
);
}
#[test]
fn skips_non_react_code() {
let source = "function add(a, b) {\n return a + b;\n}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
assert!(result.program.is_none(), "non-React code must not be transformed");
}
/// TypeScript-only constructs (`declare global`, `import =`, `export =`,
/// overload signatures, `#field in obj`) round-trip without panicking while the
/// component still compiles.
#[test]
fn typescript_only_constructs_round_trip() {
let source = "\
import legacy = require('legacy');\n\
declare global {\n interface Window { __APP__: number; }\n}\n\
declare function ambient(x: number): void;\n\
function overloaded(x: number): number;\n\
function overloaded(x: string): string;\n\
function overloaded(x: unknown): unknown { return x; }\n\
class Brand {\n #brand = 1;\n static isBrand(obj: object) { return #brand in obj; }\n}\n\
function Component(props) {\n return <div>{props.text}</div>;\n}\n\
export = legacy;\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.unwrap_or_else(|| {
panic!("component should be compiled; diagnostics: {:?}", result.diagnostics)
});
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(
output.contains("react/compiler-runtime"),
"expected compiler-runtime import in output:\n{output}"
);
assert!(output.contains("declare global"), "`declare global` lost:\n{output}");
assert!(output.contains("import legacy"), "`import =` lost:\n{output}");
assert!(
output.contains("function overloaded(x: number): number;"),
"overload signature lost:\n{output}"
);
assert!(
!output.contains("function overloaded(x: number): number {}"),
"overload signature gained an empty body:\n{output}"
);
assert!(output.contains("export = legacy"), "`export =` lost:\n{output}");
}
/// TS wrappers in assignment-target / for-head positions must not crash the
/// converter (the compiler may still decline to compile them).
#[test]
fn ts_wrapped_assignment_targets_do_not_panic() {
let cases = [
"function Component(props) {\n let x = 0;\n (x as number) = props.x;\n return <div>{x}</div>;\n}\n",
"function Component(props) {\n let x = 0;\n x! = props.x;\n return <div>{x}</div>;\n}\n",
"function Component(props) {\n const o = props.o;\n for ((o.k as string) in props.src) {}\n return <div />;\n}\n",
"function Component(props) {\n const o = props.o;\n for (o.k! of props.src) {}\n return <div />;\n}\n",
"function Component(props) {\n let [a] = props.p;\n ([a!] = props.q);\n return <div>{a}</div>;\n}\n",
];
let opts = options();
for source in cases {
let allocator = oxc_allocator::Allocator::default();
let _ = transform_source(source, oxc_span::SourceType::tsx(), &allocator, opts.clone());
}
}
/// Class bodies are stubbed by the converter and re-parsed from source on the
/// way back, so members survive.
#[test]
fn class_body_is_preserved() {
let source = "\
class Store {\n count = 0;\n increment() {\n this.count++;\n }\n}\n\
function Component(props) {\n return <div>{props.text}</div>;\n}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.expect("component should be compiled");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}");
assert!(output.contains("count = 0"), "class field lost:\n{output}");
assert!(output.contains("increment("), "class method lost:\n{output}");
}
#[test]
fn unsupported_sibling_ast_forms_are_preserved() {
let source = "\
import './style.css';\n\
export * as ns from './mod';\n\
function helper() {\n\
const C = class {\n\
method() {\n\
return 1;\n\
}\n\
};\n\
return 123n + BigInt(new C().method());\n\
}\n\
function Component(props) {\n\
return <div>{props.text}</div>;\n\
}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.expect("component should be compiled");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}");
assert!(output.contains("import \"./style.css\";"), "bare import changed:\n{output}");
assert!(
!output.contains("import {} from \"./style.css\""),
"bare import became an empty named import:\n{output}"
);
assert!(
output.contains("export * as ns from \"./mod\";"),
"namespace export lost:\n{output}"
);
assert!(output.contains("method() {"), "class expression method lost:\n{output}");
assert!(output.contains("return 1;"), "class expression body lost:\n{output}");
assert!(output.contains("123n"), "BigInt literal lost:\n{output}");
assert!(!output.contains("123nn"), "BigInt literal gained an extra suffix:\n{output}");
}
#[test]
fn typescript_surface_syntax_is_preserved_around_compiled_code() {
let source = "\
import { createContext, forwardRef } from 'react';\n\
type Props = { text: string };\n\
declare const Generic: React.FC<Props>;\n\
declare const tag: <T>(strings: TemplateStringsArray) => string;\n\
class Box<T> {}\n\
const settings = { mode: 'dark' } as const;\n\
const Context = createContext<Props | undefined>(undefined);\n\
const typedValue: string = settings.mode as string;\n\
const checked = settings.mode satisfies string;\n\
const boxed = new Box<Props>();\n\
const tagged = tag<Props>`value`;\n\
const Wrapped = forwardRef<HTMLDivElement, Props>(({ text }: Props, ref): JSX.Element => {\n\
const label: string = text satisfies string;\n\
return <div ref={ref}>{label}</div>;\n\
});\n\
function renderGeneric(props: Props): JSX.Element {\n\
'use no memo';\n\
try {\n\
return <Generic<Props> text={props.text} />;\n\
} catch (error: unknown) {\n\
return <Generic<Props> text={String(error)} />;\n\
}\n\
}\n\
function Component(props: Props): JSX.Element {\n\
return <Context.Provider value={{ text: props.text } as Props}>\n\
<Wrapped text={props.text} />\n\
</Context.Provider>;\n\
}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.unwrap_or_else(|| {
panic!("component should compile; diagnostics: {:?}", result.diagnostics)
});
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}");
assert!(output.contains("as const"), "`as const` lost:\n{output}");
assert!(
output.contains("declare const Generic: React.FC<Props>"),
"declared variable type annotation lost:\n{output}"
);
assert!(
output.contains("createContext<Props | undefined>"),
"call type arguments lost:\n{output}"
);
assert!(output.contains("typedValue: string"), "variable type annotation lost:\n{output}");
assert!(output.contains("as string"), "`as` expression lost:\n{output}");
assert!(output.contains("satisfies string"), "`satisfies` expression lost:\n{output}");
assert!(output.contains("new Box<Props>()"), "`new` type arguments lost:\n{output}");
assert!(
output.contains("tag<Props>`value`"),
"tagged template type arguments lost:\n{output}"
);
assert!(
output.contains("forwardRef<HTMLDivElement, Props>"),
"generic forwardRef call lost type arguments:\n{output}"
);
assert!(output.contains(": Props"), "parameter type lost:\n{output}");
assert!(output.contains(": JSX.Element"), "return type lost:\n{output}");
assert!(output.contains("catch (error: unknown)"), "catch type lost:\n{output}");
assert!(output.contains("<Generic<Props>"), "generic JSX type arguments lost:\n{output}");
}
#[test]
fn type_query_casts_are_renamed_with_value_bindings() {
let source = "\
type Field = { value?: string; optionsInputs?: Record<string, string> };\n\
function Component({ fields }: { fields: Field[] }) {\n\
const field = { value: 'outer', optionsInputs: {} };\n\
const nodes = fields.map((field, index) => {\n\
const options = [{ value: field.value }];\n\
const firstOptionInput = field.optionsInputs?.[options?.[0]?.value as keyof typeof field.optionsInputs];\n\
return <div key={index}>{firstOptionInput}{field.value}</div>;\n\
});\n\
return <>{nodes}{field.value}</>;\n\
}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.expect("component should be compiled");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}");
assert!(
!output.contains("typeof field.optionsInputs"),
"stale type query source was restored:\n{output}"
);
assert!(
output.contains("typeof field_0.optionsInputs"),
"type query binding was not renamed with the value binding:\n{output}"
);
}
fn rename_collision_source_and_offset() -> (&'static str, usize) {
let rename_source = "\
function makeResults(items, names) {\n\
const results = [...items.map((x) => use(x.value)), ...names.map((x) => use(x))];\n\
return results;\n\
}\n";
let collision_offset = rename_source
.find("use(x))")
.map(|index| index + "use(".len())
.expect("test source should contain the renamed reference");
(rename_source, collision_offset)
}
fn assert_has_rename_collision_setup(output: &str) {
assert!(
output.contains("function _temp2(x_0)") || output.contains("function _temp(x_0)"),
"test setup did not produce a compiler rename:\n{output}"
);
}
#[test]
fn source_extracted_class_spans_do_not_collide_with_rename_plan() {
let (rename_source, collision_offset) = rename_collision_source_and_offset();
let class_prefix = "export class C {\n m() {\n ";
let declarator_prefix = "const [octokit, ";
let padding_len = collision_offset
.checked_sub(class_prefix.len() + declarator_prefix.len())
.expect("class binding should be padded to the earlier rename position");
let source = format!(
"{rename_source}{class_prefix}{}{declarator_prefix}ghRepository] = foo();\n return ghRepository.full_name;\n }}\n}}\n",
" ".repeat(padding_len)
);
let allocator = oxc_allocator::Allocator::default();
let mut opts = options();
opts.compilation_mode = "all".to_string();
let result = transform_source(&source, oxc_span::SourceType::tsx(), &allocator, opts);
let program = result.program.expect("file should be compiled");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert_has_rename_collision_setup(&output);
assert!(
output.contains("const [octokit, ghRepository] = foo()"),
"source-preserved class binding was renamed by an unrelated plan entry:\n{output}"
);
assert!(!output.contains("const [octokit, x_0]"), "class binding was corrupted:\n{output}");
}
#[test]
fn jsx_attribute_string_entities_are_decoded() {
let source = "\
function Component(props) {\n\
return <TemplateLinkSection label={props.label} piiWarning='Use the iframe&apos;s payload.' />;\n\
}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.expect("component should be compiled");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(output.contains("react/compiler-runtime"), "component should memoize:\n{output}");
assert!(
output.contains("iframe's payload"),
"JSX string attribute entity was not decoded:\n{output}"
);
assert!(
!output.contains("iframe&apos;s payload"),
"JSX string attribute entity leaked into runtime string:\n{output}"
);
}
#[test]
fn resource_management_declarations_bail_out() {
let cases = [
"\
function Component(props) {\n using x = sideEffect();\n return <div>{props.text}</div>;\n}\n",
"\
async function Component(props) {\n await using x = sideEffect();\n return <div>{props.text}</div>;\n}\n",
];
for source in cases {
let allocator = oxc_allocator::Allocator::default();
let result =
transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
assert!(result.program.is_none(), "resource management should skip React Compiler");
assert!(
result.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
result.diagnostics
);
}
}
#[test]
fn exported_typescript_runtime_declarations_are_preserved() {
let source = "\
export enum E { A }\n\
export namespace N {\n export const value = E.A;\n}\n\
function Component(props) {\n return <div>{E.A}{N.value}{props.text}</div>;\n}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.expect("component should be compiled");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(output.contains("export enum E"), "exported enum lost:\n{output}");
assert!(output.contains("A"), "enum member lost:\n{output}");
assert!(output.contains("export namespace N"), "exported namespace lost:\n{output}");
assert!(output.contains("value = E.A"), "namespace body lost:\n{output}");
assert!(
!output.contains("declare const"),
"exported TS declaration became a placeholder:\n{output}"
);
}
/// A `React.memo(...)` component is anonymous; the prefilter must still see it.
#[test]
fn memo_wrapped_component_compiles() {
let source = "React.memo((props) => {\n return <div>{props.text}</div>;\n});\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
let program = result.program.expect("memo-wrapped component should compile");
let output = oxc_codegen::Codegen::new().build(&program).code;
assert!(output.contains("react/compiler-runtime"), "should memoize:\n{output}");
assert!(output.contains("_c("), "expected memo cache reads:\n{output}");
}
/// Diagnostics are surfaced at the compiler's own severity, not flattened.
#[test]
fn diagnostics_preserve_compiler_severity() {
use oxc_diagnostics::Severity;
// A Rules of Hooks violation is an `Error`-severity diagnostic.
let source = "function Component(props) {\n if (props.cond) {\n useState(0);\n }\n return <div>{props.text}</div>;\n}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
assert!(
result.diagnostics.iter().any(|d| d.severity == Severity::Error),
"Rules of Hooks violation should be reported as an error: {:?}",
result.diagnostics
);
// A local named `fbt` is an unsupported-syntax bail-out — a warning, not an error.
let source = "function Component() {\n const fbt = \"span\";\n return <fbt desc=\"label\">Hello</fbt>;\n}\n";
let allocator = oxc_allocator::Allocator::default();
let result = transform_source(source, oxc_span::SourceType::tsx(), &allocator, options());
assert!(
result.diagnostics.iter().any(|d| d.severity == Severity::Warning),
"fbt bail-out should be reported as a warning: {:?}",
result.diagnostics
);
assert!(
result.diagnostics.iter().all(|d| d.severity != Severity::Error),
"fbt warning must not be reported as an error: {:?}",
result.diagnostics
);
}
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
use oxc_ast::ast::{
AssignmentTarget, CallExpression, Expression, Function, Program, VariableDeclarator,
};
use oxc_ast_visit::{Visit, walk};
/// Whether the program contains a component (Uppercase name) or hook (`use[A-Z0-9]`).
pub fn has_react_like_functions(program: &Program) -> bool {
let mut visitor = ReactLikeVisitor { found: false, current_name: None };
visitor.visit_program(program);
visitor.found
}
pub fn has_resource_management_declarations(program: &Program) -> bool {
let mut visitor = ResourceManagementVisitor { found: false };
visitor.visit_program(program);
visitor.found
}
use react_compiler_hir::environment::is_react_like_name;
/// `memo`/`forwardRef` (optionally `React.`-qualified): their callback is a
/// component even when anonymous, so such files must not be skipped.
fn is_component_wrapper(callee: &Expression) -> bool {
let is_wrapper = |name: &str| matches!(name, "memo" | "forwardRef");
match callee {
Expression::Identifier(id) => is_wrapper(&id.name),
Expression::StaticMemberExpression(m) => is_wrapper(&m.property.name),
_ => false,
}
}
struct ReactLikeVisitor {
found: bool,
current_name: Option<String>,
}
struct ResourceManagementVisitor {
found: bool,
}
impl<'a> Visit<'a> for ResourceManagementVisitor {
fn visit_variable_declaration(&mut self, decl: &oxc_ast::ast::VariableDeclaration<'a>) {
if self.found {
return;
}
if decl.kind.is_using() {
self.found = true;
return;
}
walk::walk_variable_declaration(self, decl);
}
}
impl<'a> Visit<'a> for ReactLikeVisitor {
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
if self.found {
return;
}
let name = match &decl.id {
oxc_ast::ast::BindingPattern::BindingIdentifier(ident) => Some(ident.name.to_string()),
_ => None,
};
let prev_name = self.current_name.take();
self.current_name = name;
if let Some(init) = &decl.init {
self.visit_expression(init);
}
self.current_name = prev_name;
}
fn visit_assignment_expression(&mut self, expr: &oxc_ast::ast::AssignmentExpression<'a>) {
if self.found {
return;
}
let name = match &expr.left {
AssignmentTarget::AssignmentTargetIdentifier(ident) => Some(ident.name.to_string()),
_ => None,
};
let prev_name = self.current_name.take();
self.current_name = name;
self.visit_expression(&expr.right);
self.current_name = prev_name;
}
fn visit_function(&mut self, func: &Function<'a>, _flags: oxc_semantic::ScopeFlags) {
if self.found {
return;
}
if let Some(id) = &func.id {
if is_react_like_name(&id.name) {
self.found = true;
return;
}
}
if func.id.is_none() {
if let Some(name) = &self.current_name {
if is_react_like_name(name) {
self.found = true;
return;
}
}
}
// Don't traverse into the function body
}
fn visit_arrow_function_expression(
&mut self,
_expr: &oxc_ast::ast::ArrowFunctionExpression<'a>,
) {
if self.found {
return;
}
if let Some(name) = &self.current_name {
if is_react_like_name(name) {
self.found = true;
return;
}
}
// Don't traverse into the function body
}
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
if self.found {
return;
}
// A function passed to `memo`/`forwardRef` is a component even without a
// React-like name.
if is_component_wrapper(&call.callee)
&& call.arguments.iter().any(|arg| {
matches!(
arg.as_expression(),
Some(
Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_)
)
)
})
{
self.found = true;
return;
}
walk::walk_call_expression(self, call);
}
fn visit_class(&mut self, _class: &oxc_ast::ast::Class<'a>) {
// Skip class bodies entirely
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_react_like_name() {
assert!(is_react_like_name("Component"));
assert!(is_react_like_name("MyComponent"));
assert!(is_react_like_name("A"));
assert!(is_react_like_name("useState"));
assert!(is_react_like_name("useEffect"));
assert!(is_react_like_name("use0"));
assert!(!is_react_like_name("component"));
assert!(!is_react_like_name("myFunction"));
assert!(!is_react_like_name("use"));
assert!(!is_react_like_name("user"));
assert!(!is_react_like_name("useful"));
assert!(!is_react_like_name(""));
}
}
+1
View File
@@ -24,6 +24,7 @@ doctest = false
[dependencies]
oxc = { workspace = true, features = ["full"] }
oxc_napi = { workspace = true }
oxc_react_compiler = { workspace = true } # build the React Compiler `PluginOptions`
oxc_sourcemap = { workspace = true, features = ["napi"] }
rustc-hash = { workspace = true }
+99
View File
@@ -329,6 +329,97 @@ export interface PluginsOptions {
taggedTemplateEscape?: boolean
}
/** Dynamic gating for {@link ReactCompilerOptions#dynamicGating}. */
export interface ReactCompilerDynamicGating {
/** Module the gating import comes from. */
source: string
}
/** Static gating for {@link ReactCompilerOptions#gating}. */
export interface ReactCompilerGating {
/** Module the gating import comes from. */
source: string
/** Imported specifier used as the gate. */
importSpecifierName: string
}
/**
* Options for the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
*
* Mirrors the compiler's `PluginOptions`. The deep `environment` configuration
* (inference / validation flags) is not surfaced here.
*
* @see {@link TransformOptions#reactCompiler}
*/
export interface ReactCompilerOptions {
/**
* Which functions to compile.
*
* @default 'infer'
*/
compilationMode?: 'infer' | 'syntax' | 'annotation' | 'all'
/**
* What to do when a function cannot be compiled.
*
* @default 'none'
*/
panicThreshold?: 'none' | 'critical_errors' | 'all_errors'
/**
* React runtime version target. `17` and `18` require the
* `react-compiler-runtime` package; `19` ships the runtime in `react`.
*
* @default '19'
*/
target?: '17' | '18' | '19'
/**
* Analyze and report diagnostics only; emit no transformed code.
*
* @default false
*/
noEmit?: boolean
/**
* Compiler output mode.
*
* @default undefined
*/
outputMode?: 'client' | 'ssr' | 'lint'
/**
* Compile even functions marked with the `"use no memo"` / `"use no forget"`
* opt-out directives.
*
* @default false
*/
ignoreUseNoForget?: boolean
/**
* Treat Flow suppression comments as opt-outs.
*
* @default true
*/
flowSuppressions?: boolean
/**
* Enable `react-native-reanimated` support.
*
* @default false
*/
enableReanimated?: boolean
/**
* Development mode (extra validation / instrumentation).
*
* @default false
*/
isDev?: boolean
/** Source file name, used for the fast-refresh hash and in diagnostics. */
filename?: string
/** ESLint rules whose suppressions opt a function out of compilation. */
eslintSuppressionRules?: Array<string>
/** Extra directives that opt a function out of compilation. */
customOptOutDirectives?: Array<string>
/** Also emit a gated (feature-flagged) version of each compiled function. */
gating?: ReactCompilerGating
/** Dynamically-gated compilation. */
dynamicGating?: ReactCompilerDynamicGating
}
export interface ReactRefreshOptions {
/**
* Specify the identifier of the refresh registration variable.
@@ -506,6 +597,14 @@ export interface TransformOptions {
inject?: Record<string, string | [string, string]>
/** Decorator plugin */
decorator?: DecoratorOptions
/**
* Enable the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
*
* `true` enables it with default options; an object enables it with the
* given options; `false` or omitted disables it. When enabled, the compiler
* runs as the first transform and memoizes React components and hooks.
*/
reactCompiler?: boolean | ReactCompilerOptions
/**
* Third-party plugins to use.
* @see {@link https://oxc.rs/docs/guide/usage/transformer/plugins}
+3
View File
@@ -14,5 +14,8 @@ static ALLOC: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc;
mod isolated_declaration;
pub use isolated_declaration::*;
mod react_compiler;
pub use react_compiler::*;
mod transformer;
pub use transformer::*;
+167
View File
@@ -0,0 +1,167 @@
//! React Compiler options for the transform binding.
//!
//! A curated, JS-facing mirror of the React Compiler `PluginOptions` (the deep
//! `environment` config is not surfaced), resolved into the concrete options the
//! transformer consumes.
use napi::Either;
use napi_derive::napi;
/// Options for the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
///
/// Mirrors the compiler's `PluginOptions`. The deep `environment` configuration
/// (inference / validation flags) is not surfaced here.
///
/// @see {@link TransformOptions#reactCompiler}
#[napi(object)]
#[derive(Default, Debug)]
pub struct ReactCompilerOptions {
/// Which functions to compile.
///
/// @default 'infer'
#[napi(ts_type = "'infer' | 'syntax' | 'annotation' | 'all'")]
pub compilation_mode: Option<String>,
/// What to do when a function cannot be compiled.
///
/// @default 'none'
#[napi(ts_type = "'none' | 'critical_errors' | 'all_errors'")]
pub panic_threshold: Option<String>,
/// React runtime version target. `17` and `18` require the
/// `react-compiler-runtime` package; `19` ships the runtime in `react`.
///
/// @default '19'
#[napi(ts_type = "'17' | '18' | '19'")]
pub target: Option<String>,
/// Analyze and report diagnostics only; emit no transformed code.
///
/// @default false
pub no_emit: Option<bool>,
/// Compiler output mode.
///
/// @default undefined
#[napi(ts_type = "'client' | 'ssr' | 'lint'")]
pub output_mode: Option<String>,
/// Compile even functions marked with the `"use no memo"` / `"use no forget"`
/// opt-out directives.
///
/// @default false
pub ignore_use_no_forget: Option<bool>,
/// Treat Flow suppression comments as opt-outs.
///
/// @default true
pub flow_suppressions: Option<bool>,
/// Enable `react-native-reanimated` support.
///
/// @default false
pub enable_reanimated: Option<bool>,
/// Development mode (extra validation / instrumentation).
///
/// @default false
pub is_dev: Option<bool>,
/// Source file name, used for the fast-refresh hash and in diagnostics.
pub filename: Option<String>,
/// ESLint rules whose suppressions opt a function out of compilation.
pub eslint_suppression_rules: Option<Vec<String>>,
/// Extra directives that opt a function out of compilation.
pub custom_opt_out_directives: Option<Vec<String>>,
/// Also emit a gated (feature-flagged) version of each compiled function.
pub gating: Option<ReactCompilerGating>,
/// Dynamically-gated compilation.
pub dynamic_gating: Option<ReactCompilerDynamicGating>,
}
/// Static gating for {@link ReactCompilerOptions#gating}.
#[napi(object)]
#[derive(Debug)]
pub struct ReactCompilerGating {
/// Module the gating import comes from.
pub source: String,
/// Imported specifier used as the gate.
pub import_specifier_name: String,
}
/// Dynamic gating for {@link ReactCompilerOptions#dynamicGating}.
#[napi(object)]
#[derive(Debug)]
pub struct ReactCompilerDynamicGating {
/// Module the gating import comes from.
pub source: String,
}
/// Resolve the JS `reactCompiler` option into the compiler's `PluginOptions`:
/// `true` → default options, an object → those options, `false`/absent → disabled.
pub fn resolve(
option: Option<Either<bool, ReactCompilerOptions>>,
) -> Option<oxc_react_compiler::PluginOptions> {
match option {
Some(Either::A(true)) => Some(oxc_react_compiler::default_plugin_options()),
Some(Either::B(options)) => Some(options.into_plugin_options()),
Some(Either::A(false)) | None => None,
}
}
impl ReactCompilerOptions {
fn into_plugin_options(self) -> oxc_react_compiler::PluginOptions {
let mut options = oxc_react_compiler::default_plugin_options();
if let Some(compilation_mode) = self.compilation_mode {
options.compilation_mode = compilation_mode;
}
if let Some(panic_threshold) = self.panic_threshold {
options.panic_threshold = panic_threshold;
}
if let Some(target) = self.target {
options.target = oxc_react_compiler::CompilerTarget::Version(target);
}
if let Some(no_emit) = self.no_emit {
options.no_emit = no_emit;
}
if self.output_mode.is_some() {
options.output_mode = self.output_mode;
}
if let Some(ignore_use_no_forget) = self.ignore_use_no_forget {
options.ignore_use_no_forget = ignore_use_no_forget;
}
if let Some(flow_suppressions) = self.flow_suppressions {
options.flow_suppressions = flow_suppressions;
}
if let Some(enable_reanimated) = self.enable_reanimated {
options.enable_reanimated = enable_reanimated;
}
if let Some(is_dev) = self.is_dev {
options.is_dev = is_dev;
}
if self.filename.is_some() {
options.filename = self.filename;
}
if self.eslint_suppression_rules.is_some() {
options.eslint_suppression_rules = self.eslint_suppression_rules;
}
if self.custom_opt_out_directives.is_some() {
options.custom_opt_out_directives = self.custom_opt_out_directives;
}
if let Some(gating) = self.gating {
options.gating = Some(oxc_react_compiler::GatingConfig {
source: gating.source,
import_specifier_name: gating.import_specifier_name,
});
}
if let Some(dynamic_gating) = self.dynamic_gating {
options.dynamic_gating =
Some(oxc_react_compiler::DynamicGatingConfig { source: dynamic_gating.source });
}
options
}
}
+28 -1
View File
@@ -30,7 +30,7 @@ use oxc::{
use oxc_napi::{OxcError, get_source_type};
use oxc_sourcemap::napi::SourceMap;
use crate::IsolatedDeclarationsOptions;
use crate::{IsolatedDeclarationsOptions, react_compiler::ReactCompilerOptions};
#[derive(Default)]
#[napi(object)]
@@ -149,6 +149,14 @@ pub struct TransformOptions {
/// Decorator plugin
pub decorator: Option<DecoratorOptions>,
/// Enable the experimental [React Compiler](https://github.com/facebook/react/pull/36173).
///
/// `true` enables it with default options; an object enables it with the
/// given options; `false` or omitted disables it. When enabled, the compiler
/// runs as the first transform and memoizes React components and hooks.
#[napi(ts_type = "boolean | ReactCompilerOptions")]
pub react_compiler: Option<Either<bool, ReactCompilerOptions>>,
/// Third-party plugins to use.
/// @see {@link https://oxc.rs/docs/guide/usage/transformer/plugins}
pub plugins: Option<PluginsOptions>,
@@ -198,6 +206,16 @@ impl TryFrom<TransformOptions> for oxc::transformer::TransformOptions {
}
}
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 {
@@ -742,6 +760,8 @@ 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>,
@@ -794,6 +814,8 @@ 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)])?,
@@ -810,6 +832,7 @@ impl Compiler {
declaration_map: None,
define,
inject,
react_compiler,
helpers_used: FxHashMap::default(),
errors: vec![],
})
@@ -843,6 +866,10 @@ 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);
+186
View File
@@ -0,0 +1,186 @@
import { describe, expect, it } from "vitest";
import { transformSync } from "../index";
// A single fixture exercising every concern the React Compiler integration has to
// handle together: a memoizable component using a hook, TypeScript types, JSX, ES
// module syntax, and top-level comments. The compiler runs first on the pristine
// AST, then the rest of the transform pipeline (TypeScript, JSX) runs on its
// output, and codegen emits the result.
const fixture = `// @license MIT
import { useState } from "react";
interface Props {
text: string;
onClick: () => void;
}
// Memoized component: exercises hooks, TS types, JSX and comments.
export function Component(props: Props) {
const [count, setCount] = useState<number>(0);
return (
<div onClick={() => props.onClick()}>
{props.text}: {count}
</div>
);
}
`;
describe("reactCompiler", () => {
it("memoizes, composes with the TS + JSX transforms, and preserves comments", () => {
const { code, errors } = transformSync("Component.tsx", fixture, {
reactCompiler: true,
jsx: { runtime: "automatic" },
});
expect(errors).toEqual([]);
// React Compiler memoized the component.
expect(code).toContain("react/compiler-runtime");
expect(code).toContain("_c(");
// JSX was lowered via the automatic runtime — no raw JSX remains.
expect(code).toContain("jsx");
expect(code).not.toContain("<div");
// TypeScript was stripped: the interface, annotations and generic are gone.
expect(code).not.toContain("interface Props");
expect(code).not.toContain(": Props");
expect(code).not.toContain("<number>");
// The hook call and ES module syntax survive.
expect(code).toContain("useState(");
expect(code).toContain("export function Component");
// Top-level comments survive react_compiler -> transformer -> codegen.
expect(code).toContain("@license MIT");
expect(code).toContain("Memoized component");
});
it("accepts a ReactCompilerOptions object", () => {
const { code } = transformSync("Component.tsx", fixture, {
reactCompiler: { compilationMode: "all" },
});
expect(code).toContain("react/compiler-runtime");
expect(code).toContain("_c(");
});
// Each option below changes observable output, proving it is forwarded to the compiler.
it("forwards `target` — 17/18 import the standalone runtime package", () => {
const { code } = transformSync("Component.tsx", fixture, {
reactCompiler: { target: "18" },
jsx: { runtime: "automatic" },
});
expect(code).toContain("react-compiler-runtime");
expect(code).not.toContain("react/compiler-runtime");
});
it("forwards `gating` — emits a feature-gated component", () => {
const { code } = transformSync("Component.tsx", fixture, {
reactCompiler: {
gating: { source: "my-gating-module", importSpecifierName: "isForgetEnabled" },
},
jsx: { runtime: "automatic" },
});
expect(code).toContain("my-gating-module");
expect(code).toContain("isForgetEnabled");
});
it("forwards `ignoreUseNoForget` — compiles a `use no memo` function", () => {
const source = `function Component(props) {
"use no memo";
return <div>{props.text}</div>;
}
`;
const optedOut = transformSync("Component.jsx", source, {
reactCompiler: true,
jsx: { runtime: "automatic" },
});
expect(optedOut.code).not.toContain("_c(");
const overridden = transformSync("Component.jsx", source, {
reactCompiler: { ignoreUseNoForget: true },
jsx: { runtime: "automatic" },
});
expect(overridden.code).toContain("_c(");
});
it("emits code when React Compiler reports warnings", () => {
const { code, errors } = transformSync(
"Component.jsx",
`
function Component() {
const fbt = "span";
return <fbt desc="label">Hello</fbt>;
}
`,
{
reactCompiler: true,
jsx: { runtime: "automatic" },
},
);
expect(code).not.toBe("");
expect(code).toContain("function Component");
expect(errors.some((error) => error.severity === "Warning")).toBe(true);
expect(errors.some((error) => error.severity === "Error")).toBe(false);
});
it("reports React Compiler errors at error severity without aborting codegen", () => {
const { code, errors } = transformSync(
"Component.jsx",
`
function Component(props) {
if (props.cond) {
useState(0);
}
return <div>{props.text}</div>;
}
`,
{
reactCompiler: true,
jsx: { runtime: "automatic" },
},
);
// The Rules of Hooks violation is surfaced at error severity (not flattened
// to a warning)...
expect(errors.some((error) => error.severity === "Error")).toBe(true);
// ...but the React Compiler leaves a valid program, so codegen still runs
// and the component is emitted rather than the whole file being dropped.
expect(code).not.toBe("");
expect(code).toContain("function Component");
});
it("keeps enum values available for the downstream TypeScript transform", () => {
const { code, errors } = transformSync(
"Component.tsx",
`
enum E {
A = 1,
B = A + 1,
}
function Component() {
return <div>{E.B}</div>;
}
`,
{
reactCompiler: true,
jsx: { runtime: "automatic" },
},
);
expect(errors).toEqual([]);
expect(code).toContain('E[E["B"] = 2] = "B"');
});
it("does nothing when `reactCompiler` is omitted (the default) or `false`", () => {
for (const options of [{}, { reactCompiler: false }]) {
const { code } = transformSync("Component.tsx", fixture, options);
expect(code).not.toContain("react/compiler-runtime");
expect(code).not.toContain("_c(");
}
});
});
+15
View File
@@ -54,6 +54,11 @@ harness = false
name = "pipeline"
harness = false
[[bench]]
name = "react_compiler"
harness = false
required-features = ["react_compiler"]
# Only run in CI
[[bench]]
name = "parser_napi"
@@ -77,6 +82,7 @@ oxc_parser = { workspace = true, features = [
"benchmarking",
"regular_expression",
], optional = true }
oxc_react_compiler = { workspace = true, optional = true }
oxc_semantic = { workspace = true, optional = true }
oxc_span = { workspace = true, optional = true, features = ["schemars", "serialize"] }
oxc_tasks_common = { workspace = true, optional = true }
@@ -125,6 +131,14 @@ linter = [
"dep:oxc_tasks_common",
"oxc_semantic/cfg",
]
react_compiler = [
"dep:oxc_allocator",
"dep:oxc_parser",
"dep:oxc_react_compiler",
"dep:oxc_semantic",
"dep:oxc_span",
"dep:oxc_tasks_common",
]
[dev-dependencies]
rustc-hash = { workspace = true }
@@ -142,6 +156,7 @@ ignored = [
"oxc_mangler",
"oxc_minifier",
"oxc_parser",
"oxc_react_compiler",
"oxc_semantic",
"oxc_span",
"oxc_tasks_common",
+42
View File
@@ -0,0 +1,42 @@
use oxc_allocator::Allocator;
use oxc_benchmark::{BenchmarkId, Criterion, criterion_group, criterion_main};
use oxc_parser::{Parser, ParserReturn};
use oxc_react_compiler::{default_plugin_options, transform};
use oxc_semantic::SemanticBuilder;
use oxc_tasks_common::TestFiles;
fn bench_react_compiler(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("react_compiler");
for file in TestFiles::minimal().files() {
let id = BenchmarkId::from_parameter(&file.file_name);
let source_text = &file.source_text;
let source_type = file.source_type;
let options = default_plugin_options();
// Create `Allocator` outside of `bench_function`, so the same allocator is
// used for both the warmup and measurement phases.
let mut allocator = Allocator::default();
group.bench_function(id, |b| {
b.iter_with_setup_wrapper(|runner| {
// Reset allocator at start of each iteration.
allocator.reset();
// Create fresh AST + semantic data for each iteration. The compiler
// reads them and allocates the compiled program in the same arena.
let ParserReturn { program, .. } =
Parser::new(&allocator, source_text, source_type).parse();
let semantic = SemanticBuilder::new().build(&program).semantic;
runner.run(|| transform(&program, &semantic, &allocator, options.clone()));
});
});
}
group.finish();
}
criterion_group!(react_compiler, bench_react_compiler);
criterion_main!(react_compiler);