Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 256 additions & 1 deletion crates/swc_ecma_transforms_react/src/jsx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ where
.throw_if_namespace
.unwrap_or_else(default_throw_if_namespace),
top_level_node: true,
ctx_stack: vec![JsxSelfCtx::default()],
}
}

Expand Down Expand Up @@ -295,6 +296,20 @@ where
pragma_frag: Lrc<Box<Expr>>,
development: bool,
throw_if_namespace: bool,
ctx_stack: Vec<JsxSelfCtx>,
}

/// Context used for `__self` generation in development mode.
///
/// We intentionally mirror babel's behavior:
/// - inside a derived class constructor, `this` is unavailable before `super()`
/// - nested function declarations / function expressions reset constructor
/// state
/// - arrow expressions inherit constructor state from outer scope
#[derive(Clone, Copy, Default)]
struct JsxSelfCtx {
in_constructor: bool,
in_derived_class: bool,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -489,6 +504,112 @@ impl<C> Jsx<C>
where
C: Comments,
{
#[inline]
fn current_ctx(&self) -> JsxSelfCtx {
*self
.ctx_stack
.last()
.expect("jsx self context stack should never be empty")
}

#[inline]
fn push_ctx(&mut self, ctx: JsxSelfCtx) {
self.ctx_stack.push(ctx);
}

#[inline]
fn pop_ctx(&mut self) {
if self.ctx_stack.len() > 1 {
self.ctx_stack.pop();
}
}

#[inline]
fn should_inject_self(&self) -> bool {
if !self.development {
return false;
}

let ctx = self.current_ctx();
!(ctx.in_constructor && ctx.in_derived_class)
}

fn source_object_expr(&self, span: Span) -> Option<Box<Expr>> {
if !self.development || span == DUMMY_SP {
return None;
}

let loc = self.cm.lookup_char_pos(span.lo);
let file_name = loc.file.name.to_string();

Some(Box::new(
ObjectLit {
span: DUMMY_SP,
props: vec![
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("fileName")),
value: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
raw: None,
value: file_name.into(),
}))),
}))),
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("lineNumber")),
value: loc.line.into(),
}))),
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("columnNumber")),
value: (loc.col.0 + 1).into(),
}))),
],
}
.into(),
))
}

#[inline]
fn source_expr_arg(&self, span: Span) -> Option<ExprOrSpread> {
self.source_object_expr(span).map(|expr| expr.as_arg())
}

#[inline]
fn self_expr_arg(&self) -> Option<ExprOrSpread> {
if self.should_inject_self() {
Some(ThisExpr { span: DUMMY_SP }.as_arg())
} else {
None
}
}

fn append_dev_attrs(&self, attrs: &mut Vec<JSXAttrOrSpread>, opening_span: Span) {
if !self.development {
return;
}

Comment on lines +585 to +589
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev-only __source/__self injection is now implemented inside the JSX transform (source_object_expr/append_dev_attrs/should_inject_self) instead of via separate passes. There don’t appear to be JSX transform tests asserting the generated __source/__self output (or the constructor/derived-class suppression behavior) for the integrated react() pipeline. Adding a focused test case would help prevent regressions in this newly inlined logic (including the createElement fallback path).

Copilot uses AI. Check for mistakes.
if let Some(source) = self.source_object_expr(opening_span) {
attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
span: DUMMY_SP,
name: JSXAttrName::Ident(quote_ident!("__source")),
value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
span: DUMMY_SP,
expr: JSXExpr::Expr(source),
})),
}));
}

if self.should_inject_self() {
attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
span: DUMMY_SP,
name: JSXAttrName::Ident(quote_ident!("__self")),
value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
span: DUMMY_SP,
expr: JSXExpr::Expr(Box::new(ThisExpr { span: DUMMY_SP }.into())),
})),
}));
}
}

/// Process JSX attribute value, handling JSXElements and JSXFragments
fn process_attr_value(&mut self, value: Option<JSXAttrValue>) -> Box<Expr> {
match value {
Expand Down Expand Up @@ -686,6 +807,7 @@ where
fn jsx_elem_to_expr(&mut self, el: JSXElement) -> Expr {
let top_level_node = self.top_level_node;
let mut span = el.span();
let opening_span = el.opening.span;
let use_create_element = should_use_create_element(&el.opening.attrs);
self.top_level_node = false;

Expand Down Expand Up @@ -850,6 +972,36 @@ where
.map(Some)
.collect::<Vec<_>>();

if self.development {
if use_create_element {
if let Some(source) = self.source_object_expr(opening_span) {
props_obj
.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("__source")),
value: source,
}))));
}

if self.should_inject_self() {
Comment on lines +977 to +986
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the use_create_element development branch, __source/__self are appended unconditionally to props_obj. If the original JSX already included __source/__self attributes, the earlier loop will already have emitted those keys into props_obj, and this later injection will create duplicate keys (and override the user-provided value because it is appended last). Consider checking whether props_obj already contains these keys (or tracking booleans while folding attrs) before pushing the dev metadata entries.

Suggested change
if let Some(source) = self.source_object_expr(opening_span) {
props_obj
.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("__source")),
value: source,
}))));
}
if self.should_inject_self() {
let has_source_prop = props_obj.props.iter().any(|prop_or_spread| {
if let PropOrSpread::Prop(prop) = prop_or_spread {
if let Prop::KeyValue(kv) = &**prop {
if let PropName::Ident(ident) = &kv.key {
return ident.sym == *"__source";
}
}
}
false
});
let has_self_prop = props_obj.props.iter().any(|prop_or_spread| {
if let PropOrSpread::Prop(prop) = prop_or_spread {
if let Prop::KeyValue(kv) = &**prop {
if let PropName::Ident(ident) = &kv.key {
return ident.sym == *"__self";
}
}
}
false
});
if let Some(source) = self.source_object_expr(opening_span) {
if !has_source_prop {
props_obj
.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("__source")),
value: source,
}))));
}
}
if self.should_inject_self() && !has_self_prop {

Copilot uses AI. Check for mistakes.
props_obj
.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("__self")),
value: Box::new(ThisExpr { span: DUMMY_SP }.into()),
}))));
}
} else {
if source_props.is_none() {
source_props = self.source_expr_arg(opening_span);
}

if self_props.is_none() {
self_props = self.self_expr_arg();
}
}
}

let use_jsxs = match children.len() {
0 => false,
1 if matches!(children.first(), Some(Some(child)) if child.spread.is_none()) => {
Expand Down Expand Up @@ -952,9 +1104,12 @@ where
// Build args Vec directly for better performance
let children_capacity = el.children.len();
let mut args = Vec::with_capacity(2 + children_capacity);
let mut attrs = el.opening.attrs;

self.append_dev_attrs(&mut attrs, opening_span);

args.push(name.as_arg());
args.push(self.fold_attrs_for_classic(el.opening.attrs).as_arg());
args.push(self.fold_attrs_for_classic(attrs).as_arg());

// Add children
for child in el.children {
Expand Down Expand Up @@ -1160,6 +1315,106 @@ impl<C> VisitMutHook<()> for Jsx<C>
where
C: Comments,
{
fn enter_class(&mut self, n: &mut Class, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_derived_class = n.super_class.is_some();
self.push_ctx(new_ctx);
}

fn exit_class(&mut self, _n: &mut Class, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_fn_decl(&mut self, _n: &mut FnDecl, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_fn_decl(&mut self, _n: &mut FnDecl, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_fn_expr(&mut self, _n: &mut FnExpr, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_fn_expr(&mut self, _n: &mut FnExpr, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_getter_prop(&mut self, _n: &mut GetterProp, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_getter_prop(&mut self, _n: &mut GetterProp, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_setter_prop(&mut self, _n: &mut SetterProp, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_setter_prop(&mut self, _n: &mut SetterProp, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_method_prop(&mut self, _n: &mut MethodProp, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_method_prop(&mut self, _n: &mut MethodProp, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_constructor(&mut self, _n: &mut Constructor, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = true;
self.push_ctx(new_ctx);
}

fn exit_constructor(&mut self, _n: &mut Constructor, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_class_method(&mut self, _n: &mut ClassMethod, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_class_method(&mut self, _n: &mut ClassMethod, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_private_method(&mut self, _n: &mut PrivateMethod, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_private_method(&mut self, _n: &mut PrivateMethod, _ctx: &mut ()) {
self.pop_ctx();
}

fn enter_static_block(&mut self, _n: &mut StaticBlock, _ctx: &mut ()) {
let mut new_ctx = self.current_ctx();
new_ctx.in_constructor = false;
self.push_ctx(new_ctx);
}

fn exit_static_block(&mut self, _n: &mut StaticBlock, _ctx: &mut ()) {
self.pop_ctx();
}

/// Called after visiting children of an expression.
Comment on lines +1414 to 1418
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment for exit_expr just below this point still says jsx_src/jsx_self have already added __source/__self. After inlining dev metadata generation into the JSX hook, that comment is now misleading and should be updated/removed to match the new flow.

Copilot uses AI. Check for mistakes.
///
/// This is where we transform JSX syntax to JavaScript function calls.
Expand Down
37 changes: 17 additions & 20 deletions crates/swc_ecma_transforms_react/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,29 +92,26 @@ where
let refresh_options = options.refresh.take();

let hook = CompositeHook {
first: jsx_src::hook(development, cm.clone()),
first: refresh::hook(
development,
refresh_options.clone(),
cm.clone(),
comments.clone(),
top_level_mark,
),
second: CompositeHook {
first: jsx_self::hook(development),
first: jsx::hook(
cm.clone(),
comments.clone(),
options,
top_level_mark,
unresolved_mark,
),
second: CompositeHook {
first: refresh::hook(
development,
refresh_options.clone(),
cm.clone(),
comments.clone(),
top_level_mark,
),
first: display_name::hook(),
second: CompositeHook {
first: jsx::hook(
cm.clone(),
comments.clone(),
options,
top_level_mark,
unresolved_mark,
),
second: CompositeHook {
first: display_name::hook(),
second: pure_annotations::hook(comments.clone()),
},
first: pure_annotations::hook(comments.clone()),
second: swc_ecma_hooks::NoopHook,
},
},
},
Expand Down
Loading