fix(react_compiler): preserve hook spans for React Refresh (#25393)

Fix React Refresh when used with the React Compiler N-API transform by
preserving original hook argument and binding-pattern spans through
compiler codegen.

This prevents refresh signature generation from panicking and keeps
signatures based on the original source.

Validated with `just ready` and the React transform N-API suite.

Generated with AI assistance; reviewed and validated by the contributor.
This commit is contained in:
Boshen authored and GitHub committed 2026-08-08 08:46:36 +00:00
1 parent 4a6542bfda
commit 7d8a9b5f2f
5 files changed
+56 -24

No files matched your search

@@ -1010,6 +1010,7 @@ pub struct SpreadPattern {
#[derive(Debug)]
pub struct ArrayPattern<'a> {
pub items: ArenaVec<'a, ArrayPatternElement>,
pub span: Option<Span>,
}
#[derive(Debug, Clone, Copy)]
@@ -1022,6 +1023,7 @@ pub enum ArrayPatternElement {
#[derive(Debug)]
pub struct ObjectPattern<'a> {
pub properties: ArenaVec<'a, ObjectPropertyOrSpread<'a>>,
pub span: Option<Span>,
}
#[derive(Debug, Clone, Copy)]
@@ -1884,14 +1886,14 @@ impl<'a> CloneIn<'a> for Pattern<'a> {
impl<'a> CloneIn<'a> for ArrayPattern<'a> {
type Cloned = ArrayPattern<'a>;
fn clone_in_impl(&self, sem: CloneInSemanticIds, alloc: &'a Allocator) -> Self {
ArrayPattern { items: self.items.clone_in_impl(sem, alloc) }
ArrayPattern { items: self.items.clone_in_impl(sem, alloc), span: self.span }
}
}
impl<'a> CloneIn<'a> for ObjectPattern<'a> {
type Cloned = ObjectPattern<'a>;
fn clone_in_impl(&self, sem: CloneInSemanticIds, alloc: &'a Allocator) -> Self {
ObjectPattern { properties: self.properties.clone_in_impl(sem, alloc) }
ObjectPattern { properties: self.properties.clone_in_impl(sem, alloc), span: self.span }
}
}
@@ -1422,7 +1422,10 @@ fn lower_binding_assignment<'a>(
let temporary = lower_value_to_temporary(
builder,
InstructionValue::Destructure {
lvalue: LValuePattern { pattern: Pattern::Array(ArrayPattern { items }), kind },
lvalue: LValuePattern {
pattern: Pattern::Array(ArrayPattern { items, span: Some(pattern.span) }),
kind,
},
value,
span: Some(span),
},
@@ -1576,7 +1579,10 @@ fn lower_binding_assignment<'a>(
builder,
InstructionValue::Destructure {
lvalue: LValuePattern {
pattern: Pattern::Object(ObjectPattern { properties }),
pattern: Pattern::Object(ObjectPattern {
properties,
span: Some(pattern.span),
}),
kind,
},
value,
@@ -2018,7 +2024,10 @@ fn lower_assignment_target<'a>(
let temporary = lower_value_to_temporary(
builder,
InstructionValue::Destructure {
lvalue: LValuePattern { pattern: Pattern::Array(ArrayPattern { items }), kind },
lvalue: LValuePattern {
pattern: Pattern::Array(ArrayPattern { items, span: Some(pattern.span) }),
kind,
},
value,
span: Some(span),
},
@@ -2289,7 +2298,10 @@ fn lower_assignment_target<'a>(
builder,
InstructionValue::Destructure {
lvalue: LValuePattern {
pattern: Pattern::Object(ObjectPattern { properties }),
pattern: Pattern::Object(ObjectPattern {
properties,
span: Some(pattern.span),
}),
kind,
},
value,
@@ -609,7 +609,7 @@ fn emit_destructure_props<'a>(
lvalue,
value: InstructionValue::Destructure {
lvalue: LValuePattern {
pattern: Pattern::Object(ObjectPattern { properties }),
pattern: Pattern::Object(ObjectPattern { properties, span: None }),
kind: InstructionKind::Let,
},
value: *props_obj,
@@ -48,7 +48,7 @@ use crate::react_compiler_hir::reactive::ReactiveTerminal;
use crate::react_compiler_hir::reactive::ReactiveTerminalTargetKind;
use crate::react_compiler_hir::reactive::ReactiveValue;
use oxc_diagnostics::OxcDiagnostic;
use oxc_span::Span;
use oxc_span::{GetSpanMut, Span};
use crate::react_compiler_reactive_scopes::build_reactive_function::build_reactive_function;
use crate::react_compiler_reactive_scopes::prune_hoisted_contexts::prune_hoisted_contexts;
@@ -1130,10 +1130,10 @@ fn ox_extract_for_in_of_lval<'a>(
) -> Result<(oxc::BindingPattern<'a>, oxc::VariableDeclarationKind), OxcDiagnostic> {
let (lval, kind) = match instr_value {
InstructionValue::StoreLocal { lvalue, .. } => {
(ox_codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place))?, lvalue.kind)
(ox_codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place), span)?, lvalue.kind)
}
InstructionValue::Destructure { lvalue, .. } => {
(ox_codegen_lvalue(cx, &LvalueRef::Pattern(&lvalue.pattern))?, lvalue.kind)
(ox_codegen_lvalue(cx, &LvalueRef::Pattern(&lvalue.pattern), span)?, lvalue.kind)
}
InstructionValue::StoreContext { .. } => {
cx.record_error(
@@ -1363,11 +1363,11 @@ fn ox_emit_store<'a>(
instr.span,
));
}
let lval = ox_codegen_lvalue(cx, lvalue)?;
let lval = ox_codegen_lvalue(cx, lvalue, instr.span)?;
Ok(Some(ox_make_var_decl(cx, oxc::VariableDeclarationKind::Const, lval, value)))
}
InstructionKind::Function => {
let lval = ox_codegen_lvalue(cx, lvalue)?;
let lval = ox_codegen_lvalue(cx, lvalue, instr.span)?;
let oxc::BindingPattern::BindingIdentifier(fn_id) = lval else {
return Err(invariant_err(
"Expected an identifier as function declaration lvalue",
@@ -1413,14 +1413,14 @@ fn ox_emit_store<'a>(
instr.span,
));
}
let lval = ox_codegen_lvalue(cx, lvalue)?;
let lval = ox_codegen_lvalue(cx, lvalue, instr.span)?;
Ok(Some(ox_make_var_decl(cx, oxc::VariableDeclarationKind::Let, lval, value)))
}
InstructionKind::Reassign => {
let Some(rhs) = value else {
return Err(invariant_err("Expected a value for reassignment", None));
};
let lval = ox_codegen_lvalue(cx, lvalue)?;
let lval = ox_codegen_lvalue(cx, lvalue, instr.span)?;
let target = ox_binding_pattern_to_assignment_target(cx, lval)?;
let expr = oxc_ast::ast::Expression::new_assignment_expression(
SPAN,
@@ -1924,13 +1924,13 @@ fn ox_codegen_base_instruction_value<'a>(
SPAN, *operation, true, target, &cx.ast,
)))
}
InstructionValue::StoreLocal { lvalue, value, .. } => {
InstructionValue::StoreLocal { lvalue, value, span } => {
invariant(
lvalue.kind == InstructionKind::Reassign,
"Unexpected StoreLocal in codegenInstructionValue",
None,
)?;
let lval = ox_codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place))?;
let lval = ox_codegen_lvalue(cx, &LvalueRef::Place(&lvalue.place), *span)?;
let target = ox_binding_pattern_to_assignment_target(cx, lval)?;
let rhs = ox_codegen_place_to_expression(cx, value)?;
Ok(OxValue::Expression(oxc_ast::ast::Expression::new_assignment_expression(
@@ -2145,7 +2145,14 @@ fn ox_codegen_place<'a>(
let declaration_id = ident.declaration_id;
if let Some(tmp) = cx.temp.get(&declaration_id) {
if let Some(val) = tmp {
return Ok(val.clone_in_with_semantic_ids(cx.ast.allocator()));
let mut value = val.clone_in_with_semantic_ids(cx.ast.allocator());
if let Some(span) = place.span {
match &mut value {
OxValue::Expression(expr) => *expr.span_mut() = span,
OxValue::JsxText(text) => text.span = span,
}
}
return Ok(value);
}
} else if ident.name.is_none() {
return Err(invariant_err(
@@ -2158,7 +2165,7 @@ fn ox_codegen_place<'a>(
}
let name = ox_identifier_name(cx.env, place.identifier)?;
Ok(OxValue::Expression(oxc_ast::ast::Expression::new_identifier(
SPAN,
place.span.unwrap_or(SPAN),
ox_str(&cx.ast, &name),
&cx.ast,
)))
@@ -2167,14 +2174,24 @@ fn ox_codegen_place<'a>(
fn ox_codegen_lvalue<'a>(
cx: &mut OxcContext<'a, '_>,
pattern: &LvalueRef,
fallback_span: Option<Span>,
) -> Result<oxc::BindingPattern<'a>, OxcDiagnostic> {
match pattern {
LvalueRef::Place(place) => ox_binding_for_identifier(cx, place.identifier),
let (pattern, span) = match pattern {
LvalueRef::Place(place) => {
(ox_binding_for_identifier(cx, place.identifier), place.span.or(fallback_span))
}
LvalueRef::Pattern(pat) => match pat {
Pattern::Array(arr) => ox_codegen_array_pattern(cx, arr),
Pattern::Object(obj) => ox_codegen_object_pattern(cx, obj),
Pattern::Array(arr) => (ox_codegen_array_pattern(cx, arr), arr.span.or(fallback_span)),
Pattern::Object(obj) => {
(ox_codegen_object_pattern(cx, obj), obj.span.or(fallback_span))
}
},
};
let mut pattern = pattern?;
if let Some(span) = span {
*pattern.span_mut() = span;
}
Ok(pattern)
}
fn ox_codegen_array_pattern<'a>(
+3 -2
View File
@@ -357,12 +357,13 @@ describe("transformSync", () => {
it("supports React Fast Refresh through JSX options", () => {
const result = transformSync("Component.tsx", fixture, {
reactCompiler: false,
jsx: { refresh: true },
jsx: { refresh: { emitFullSignatures: true } },
});
expect(result.errors).toEqual([]);
expect(result.code).toContain("react/compiler-runtime");
expect(result.code).toContain("$RefreshSig$");
expect(result.code).toContain("$RefreshReg$");
expect(result.code).toContain("useState{[count, setCount](0)}");
});
});