From f0718d18bd1df31d3922b194fa10750008410c51 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:28:05 +0800 Subject: [PATCH 1/2] fix react transform children prop scope --- .github/react-transform.instructions.md | 1 + .../crates/swc_plugin_snapshot/lib.rs | 84 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/.github/react-transform.instructions.md b/.github/react-transform.instructions.md index 6a0623319c..0c237fcd26 100644 --- a/.github/react-transform.instructions.md +++ b/.github/react-transform.instructions.md @@ -9,3 +9,4 @@ Expose recorded columns as 1-based values so `uiSourceMapRecords` can be fed dir When compat wraps a component with a synthetic ``, preserve the original component spans on the generated wrapper instead of using `DUMMY_SP` or `Default::default()`. Snapshot ui source map extraction reads `opening.span`, so preserved spans keep `uiSourceMapRecords` file, line, and column data intact. Keep `snapshot.filename` stable for snapshot hashing semantics, even when callers want absolute paths in exported debug metadata. If `uiSourceMapRecords.filename` needs to use the top-level transform filename, inject it at the `react/transform/src/lib.rs` boundary instead of changing the snapshot plugin's internal filename. If `swc_plugin_snapshot::JSXTransformer::new` gains a new constructor parameter, update every external callsite under `packages/react/transform/**` at the same time, including wrapper crates such as `swc-plugin-reactlynx`, not just the main `packages/react/transform/src/lib.rs` entrypoint. +In `swc_plugin_snapshot::DynamicPartExtractor`, native JSX elements manually inspect and extract opening attributes before recursing into children. After that point, recurse with `n.children.visit_mut_with(...)` instead of `n.visit_mut_children_with(...)`; the latter also revisits opening attrs and can incorrectly extract JSX inside prop expressions, such as `children={items.map(...)}`, into parent snapshot slots outside the expression's lexical scope. diff --git a/packages/react/transform/crates/swc_plugin_snapshot/lib.rs b/packages/react/transform/crates/swc_plugin_snapshot/lib.rs index 662eec1295..6b390851bf 100644 --- a/packages/react/transform/crates/swc_plugin_snapshot/lib.rs +++ b/packages/react/transform/crates/swc_plugin_snapshot/lib.rs @@ -990,10 +990,10 @@ where let pre_parent_element = self.parent_element.take(); self.parent_element = Some(el.clone()); - n.visit_mut_children_with(self); + n.children.visit_mut_with(self); self.parent_element = pre_parent_element; } else { - n.visit_mut_children_with(self.dynamic_part_visitor); + n.children.visit_mut_with(self.dynamic_part_visitor); let children_expr = jsx_children_to_expr(n.children.take()); if jsx_is_list(n) { self @@ -1655,7 +1655,11 @@ mod tests { common::{comments::SingleThreadedComments, Mark}, ecma::{ parser::{EsSyntax, Syntax}, - transforms::{base::resolver, react, testing::test}, + transforms::{ + base::resolver, + react, + testing::{test, Tester}, + }, visit::visit_mut_pass, }, }; @@ -1663,6 +1667,80 @@ mod tests { use crate::JSXTransformer; use swc_plugins_shared::{target::TransformTarget, transform_mode::TransformMode}; + #[test] + fn should_keep_jsx_in_children_prop_map_callback_scope() { + Tester::run(|tester| { + let top_level_mark = Mark::new(); + let unresolved_mark = Mark::new(); + + let program = tester.apply_transform( + ( + resolver(unresolved_mark, top_level_mark, true), + visit_mut_pass(JSXTransformer::<&SingleThreadedComments>::new( + super::JSXTransformerConfig { + preserve_jsx: false, + target: TransformTarget::MIXED, + ..Default::default() + }, + None, + TransformMode::Test, + Some(tester.cm.clone()), + )), + react::react::<&SingleThreadedComments>( + tester.cm.clone(), + None, + react::Options { + next: Some(false), + runtime: Some(react::Runtime::Automatic), + import_source: Some("@lynx-js/react".into()), + pragma: None, + pragma_frag: None, + throw_if_namespace: None, + development: Some(false), + refresh: None, + ..Default::default() + }, + top_level_mark, + unresolved_mark, + ), + ), + "input.js", + Syntax::Es(EsSyntax { + jsx: true, + ..Default::default() + }), + Some(true), + r#" + const render_data_array = ['1', '2', '3']; + + export function App() { + return ( + ( + {array123[index123]} + ))} + /> + ); + } + "#, + )?; + + let comments = tester.comments.clone(); + let output = tester.print(&program, &comments); + assert!( + output.contains("render_data_array.map((item, index123, array123)=>") + && output.contains("_jsx(\"text\""), + "the JSX in children={{...}} should stay inside the map callback; output:\n{output}", + ); + assert!( + !output.contains("$0: array123[index123]"), + "snapshot child slots must not capture map callback locals outside their scope; output:\n{output}", + ); + + Ok(()) + }); + } + test!( module, Syntax::Es(EsSyntax { From 50dcd1f96f830c8f96056d344e6420322c7095ee Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:31:43 +0800 Subject: [PATCH 2/2] add changeset for react transform scope fix --- .changeset/fix-react-children-map-scope.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fix-react-children-map-scope.md diff --git a/.changeset/fix-react-children-map-scope.md b/.changeset/fix-react-children-map-scope.md new file mode 100644 index 0000000000..24d2f5018d --- /dev/null +++ b/.changeset/fix-react-children-map-scope.md @@ -0,0 +1,6 @@ +--- +"@lynx-js/react": patch +"@lynx-js/react-rsbuild-plugin": patch +--- + +Fix stale callback-local references when transforming JSX inside `children={array.map(...)}` prop expressions.