mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
@@ -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 ↔ `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(¶m.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(¶m.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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'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'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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(""));
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
Vendored
+99
@@ -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}
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user