diff --git a/Cargo.lock b/Cargo.lock
index b67c641c44..7d8b26c4af 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -391,6 +391,17 @@ dependencies = [
"static_assertions",
]
+[[package]]
+name = "console"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "convert_case"
version = "0.6.0"
@@ -628,6 +639,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -1091,6 +1108,19 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "insta"
+version = "1.47.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e"
+dependencies = [
+ "console",
+ "once_cell",
+ "serde",
+ "similar",
+ "tempfile",
+]
+
[[package]]
name = "is-macro"
version = "0.3.5"
@@ -1302,6 +1332,8 @@ dependencies = [
"ctor",
"napi-sys",
"once_cell",
+ "serde",
+ "serde_json",
"thread_local",
]
@@ -1746,6 +1778,7 @@ dependencies = [
"swc_plugin_define_dce",
"swc_plugin_directive_dce",
"swc_plugin_dynamic_import",
+ "swc_plugin_element_template",
"swc_plugin_inject",
"swc_plugin_list",
"swc_plugin_shake",
@@ -2065,6 +2098,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+[[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
[[package]]
name = "siphasher"
version = "0.3.10"
@@ -2341,6 +2380,7 @@ dependencies = [
"swc_common",
"swc_ecma_ast",
"swc_ecma_codegen",
+ "swc_ecma_minifier",
"swc_ecma_parser",
"swc_ecma_quote_macros",
"swc_ecma_transforms_base",
@@ -3187,6 +3227,24 @@ dependencies = [
"swc_plugins_shared",
]
+[[package]]
+name = "swc_plugin_element_template"
+version = "0.1.0"
+dependencies = [
+ "convert_case 0.8.0",
+ "hex",
+ "insta",
+ "napi",
+ "napi-derive",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "sha-1",
+ "swc_core",
+ "swc_plugins_shared",
+]
+
[[package]]
name = "swc_plugin_inject"
version = "0.1.0"
@@ -3247,6 +3305,7 @@ dependencies = [
"swc_plugin_define_dce",
"swc_plugin_directive_dce",
"swc_plugin_dynamic_import",
+ "swc_plugin_element_template",
"swc_plugin_inject",
"swc_plugin_list",
"swc_plugin_shake",
diff --git a/packages/react/transform/Cargo.toml b/packages/react/transform/Cargo.toml
index 7317a0f6d2..055904c42e 100644
--- a/packages/react/transform/Cargo.toml
+++ b/packages/react/transform/Cargo.toml
@@ -11,7 +11,7 @@ crate-type = ["cdylib"]
convert_case = { workspace = true }
hex = { workspace = true }
indexmap = { workspace = true }
-napi = { workspace = true }
+napi = { workspace = true, features = ["serde-json"] }
napi-derive = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
@@ -25,6 +25,7 @@ swc_plugin_css_scope = { path = "./crates/swc_plugin_css_scope", features = ["na
swc_plugin_define_dce = { path = "./crates/swc_plugin_define_dce", features = ["napi"] }
swc_plugin_directive_dce = { path = "./crates/swc_plugin_directive_dce", features = ["napi"] }
swc_plugin_dynamic_import = { path = './crates/swc_plugin_dynamic_import', features = ["napi"] }
+swc_plugin_element_template = { path = "./crates/swc_plugin_element_template", features = ["napi"] }
swc_plugin_inject = { path = "./crates/swc_plugin_inject", features = ["napi"] }
swc_plugin_list = { path = "./crates/swc_plugin_list", features = ["napi"] }
swc_plugin_shake = { path = './crates/swc_plugin_shake', features = ["napi"] }
diff --git a/packages/react/transform/__test__/fixture.spec.js b/packages/react/transform/__test__/fixture.spec.js
index d2deb4c94e..160f5c5802 100644
--- a/packages/react/transform/__test__/fixture.spec.js
+++ b/packages/react/transform/__test__/fixture.spec.js
@@ -164,6 +164,65 @@ describe('ui source map', () => {
});
});
+describe('element template', () => {
+ it('should export compiled element templates when enabled', async () => {
+ const result = await transformReactLynx('const node = ;', {
+ mode: 'test',
+ pluginName: '',
+ filename: 'test.js',
+ sourcemap: false,
+ cssScope: false,
+ elementTemplate: {
+ preserveJsx: false,
+ runtimePkg: '@lynx-js/react',
+ filename: 'test.js',
+ target: 'LEPUS',
+ },
+ jsx: true,
+ directiveDCE: false,
+ defineDCE: false,
+ shake: false,
+ compat: true,
+ worklet: false,
+ refresh: false,
+ });
+
+ expect(Array.isArray(result.elementTemplates)).toBe(true);
+ const template = result.elementTemplates?.[0];
+ expect(template?.templateId).toEqual(expect.any(String));
+ expect(template?.templateId.length).toBeGreaterThan(0);
+ expect(template?.compiledTemplate).toEqual(expect.any(Object));
+ expect(Array.isArray(template?.compiledTemplate)).toBe(false);
+ expect(template?.sourceFile).toEqual(expect.any(String));
+ });
+
+ it('should not bridge legacy snapshot ET flags into the new ET plugin path', async () => {
+ const result = await transformReactLynx('const node = ;', {
+ mode: 'test',
+ pluginName: '',
+ filename: 'test.js',
+ sourcemap: false,
+ cssScope: false,
+ snapshot: {
+ preserveJsx: false,
+ runtimePkg: '@lynx-js/react',
+ filename: 'test.js',
+ target: 'LEPUS',
+ enableElementTemplate: true,
+ },
+ jsx: true,
+ directiveDCE: false,
+ defineDCE: false,
+ shake: false,
+ compat: true,
+ worklet: false,
+ refresh: false,
+ });
+
+ expect(result.elementTemplates).toBeUndefined();
+ });
+});
+
describe('jsx', () => {
it('should allow JSXNamespace', async () => {
const result = await transformReactLynx('const jsx = ', {
diff --git a/packages/react/transform/crates/swc_plugin_element_template/Cargo.toml b/packages/react/transform/crates/swc_plugin_element_template/Cargo.toml
new file mode 100644
index 0000000000..12500f66c0
--- /dev/null
+++ b/packages/react/transform/crates/swc_plugin_element_template/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "swc_plugin_element_template"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "lib.rs"
+
+[features]
+napi = ["dep:napi", "dep:napi-derive", "swc_plugins_shared/napi"]
+
+[dependencies]
+convert_case = { workspace = true }
+hex = { workspace = true }
+napi = { workspace = true, optional = true, features = ["serde-json"] }
+napi-derive = { workspace = true, optional = true }
+once_cell = { workspace = true }
+regex = { workspace = true }
+serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true, features = ["preserve_order"] }
+sha-1 = { workspace = true }
+swc_core = { workspace = true, features = ["base", "ecma_codegen", "ecma_parser", "ecma_minifier", "ecma_transforms_typescript", "ecma_utils", "ecma_quote", "ecma_transforms_react", "ecma_transforms_optimization", "__visit", "__testing_transform"] }
+swc_plugins_shared = { path = "../swc_plugins_shared" }
+
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(swc_ast_unknown)'] }
+
+[dev-dependencies]
+insta = { version = "1.34", features = ["json"] }
diff --git a/packages/react/transform/crates/swc_plugin_element_template/asset.rs b/packages/react/transform/crates/swc_plugin_element_template/asset.rs
new file mode 100644
index 0000000000..16c62fcf8a
--- /dev/null
+++ b/packages/react/transform/crates/swc_plugin_element_template/asset.rs
@@ -0,0 +1,64 @@
+use serde::Serialize;
+use swc_core::common::comments::Comments;
+
+use super::JSXTransformer;
+
+#[derive(Serialize, Debug, Clone)]
+pub struct ElementTemplateAsset {
+ // The compiled template is exported out-of-band from the JS module. The JS
+ // output keeps using a synthetic component tag so existing React/SWC passes can
+ // continue to own expression lowering and runtime value transport.
+ pub template_id: String,
+ pub compiled_template: serde_json::Value,
+ pub source_file: String,
+}
+
+const BUILTIN_RAW_TEXT_TEMPLATE_ID: &str = "__et_builtin_raw_text__";
+
+impl JSXTransformer
+where
+ C: Comments + Clone,
+{
+ fn builtin_raw_text_template_asset(&self) -> ElementTemplateAsset {
+ ElementTemplateAsset {
+ template_id: BUILTIN_RAW_TEXT_TEMPLATE_ID.to_string(),
+ compiled_template: serde_json::json!({
+ "kind": "element",
+ "type": "raw-text",
+ "attributesArray": [
+ {
+ "kind": "attribute",
+ "key": "text",
+ "binding": "slot",
+ "attrSlotIndex": 0,
+ }
+ ],
+ "children": [],
+ }),
+ source_file: self.cfg.filename.clone(),
+ }
+ }
+
+ pub(super) fn ensure_builtin_element_templates(&self) {
+ let Some(element_templates) = &self.element_templates else {
+ return;
+ };
+
+ let mut templates = element_templates.borrow_mut();
+ if templates.is_empty() {
+ return;
+ }
+
+ // Raw text can also appear as dynamic element-slot content. Emitting the
+ // builtin template only when user ET templates exist keeps non-ET transforms
+ // free of template metadata while giving runtime a stable key for text slots.
+ if templates
+ .iter()
+ .any(|template| template.template_id == BUILTIN_RAW_TEXT_TEMPLATE_ID)
+ {
+ return;
+ }
+
+ templates.push(self.builtin_raw_text_template_asset());
+ }
+}
diff --git a/packages/react/transform/crates/swc_plugin_element_template/attr_name.rs b/packages/react/transform/crates/swc_plugin_element_template/attr_name.rs
new file mode 100644
index 0000000000..58cd85edf2
--- /dev/null
+++ b/packages/react/transform/crates/swc_plugin_element_template/attr_name.rs
@@ -0,0 +1,89 @@
+use regex::Regex;
+
+use swc_core::ecma::ast::*;
+
+#[derive(Debug, Clone)]
+pub enum AttrName {
+ Attr(String),
+ Dataset(String),
+ Event(String, String),
+ WorkletEvent(
+ /* worklet_type */ String,
+ /* event_type */ String,
+ /* event_name */ String,
+ ),
+ Style,
+ Class,
+ ID,
+ Ref,
+ TimingFlag,
+ WorkletRef(/* worklet_type */ String),
+ ListItemPlatformInfo,
+ Gesture(String),
+}
+
+impl From for AttrName {
+ fn from(name: String) -> Self {
+ if let Some(stripped_name) = name.strip_prefix("data-") {
+ AttrName::Dataset(stripped_name.to_string())
+ } else if name == "class" || name == "className" {
+ AttrName::Class
+ } else if name == "style" {
+ AttrName::Style
+ } else if name == "id" {
+ AttrName::ID
+ } else if name == "ref" {
+ AttrName::Ref
+ } else if name == "__lynx_timing_flag" {
+ AttrName::TimingFlag
+ } else if let Some((event_type, event_name)) = get_event_type_and_name(name.as_str()) {
+ AttrName::Event(event_type, event_name)
+ } else {
+ AttrName::Attr(name)
+ }
+ }
+}
+
+impl From for AttrName {
+ fn from(name: Str) -> Self {
+ let name = name.value.to_string_lossy().into_owned();
+ Self::from(name)
+ }
+}
+
+impl From for AttrName {
+ fn from(name: Ident) -> Self {
+ let name = name.sym.as_ref().to_string();
+ Self::from(name)
+ }
+}
+
+impl AttrName {
+ pub fn from_ns(ns: Ident, name: Ident) -> Self {
+ let ns_str = ns.sym.as_ref().to_string();
+ let name_str = name.sym.as_ref().to_string();
+ if name_str == "ref" {
+ AttrName::WorkletRef(ns_str)
+ } else if let Some((event_type, event_name)) = get_event_type_and_name(name_str.as_str()) {
+ AttrName::WorkletEvent(ns_str, event_type, event_name)
+ } else if name_str == "gesture" {
+ AttrName::Gesture(ns_str)
+ } else {
+ todo!()
+ }
+ }
+}
+
+fn get_event_type_and_name(props_key: &str) -> Option<(String, String)> {
+ let re = Regex::new(r"^(global-bind|bind|catch|capture-bind|capture-catch)([A-Za-z]+)$").unwrap();
+ if let Some(captures) = re.captures(props_key) {
+ let event_type = if captures.get(1).unwrap().as_str().contains("capture") {
+ captures.get(1).unwrap().as_str().to_string()
+ } else {
+ format!("{}Event", captures.get(1).unwrap().as_str())
+ };
+ let event_name = captures.get(2).unwrap().as_str().to_string();
+ return Some((event_type, event_name));
+ }
+ None
+}
diff --git a/packages/react/transform/crates/swc_plugin_element_template/lib.rs b/packages/react/transform/crates/swc_plugin_element_template/lib.rs
new file mode 100644
index 0000000000..f5eb3be324
--- /dev/null
+++ b/packages/react/transform/crates/swc_plugin_element_template/lib.rs
@@ -0,0 +1,3234 @@
+use serde::Deserialize;
+use std::{
+ cell::RefCell,
+ collections::{HashMap, HashSet},
+ rc::Rc,
+};
+
+use once_cell::sync::Lazy;
+use swc_core::{
+ common::{
+ comments::{CommentKind, Comments},
+ errors::HANDLER,
+ sync::Lrc,
+ util::take::Take,
+ Mark, SourceMap, Span, Spanned, SyntaxContext, DUMMY_SP,
+ },
+ ecma::{
+ ast::{JSXExpr, *},
+ utils::{is_literal, prepend_stmt, private_ident},
+ visit::{VisitMut, VisitMutWith},
+ },
+ quote, quote_expr,
+};
+
+mod asset;
+mod attr_name;
+mod slot;
+mod template_definition;
+
+pub use self::asset::ElementTemplateAsset;
+use self::slot::{expr_to_jsx_child, lower_lepus_et_children_expr, wrap_in_slot};
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct UISourceMapRecord {
+ pub ui_source_map: i32,
+ pub line_number: u32,
+ pub column_number: u32,
+ pub snapshot_id: String,
+}
+
+pub type ElementTemplateTransformerConfig = JSXTransformerConfig;
+pub type ElementTemplateTransformer = JSXTransformer;
+pub type ElementTemplateUISourceMapRecord = UISourceMapRecord;
+
+#[cfg(feature = "napi")]
+pub mod napi;
+
+use swc_plugins_shared::{
+ css::get_string_inline_style_from_literal,
+ jsx_helpers::{
+ jsx_attr_name, jsx_attr_to_prop, jsx_attr_value, jsx_children_to_expr, jsx_has_dynamic_key,
+ jsx_is_children_full_dynamic, jsx_is_custom, jsx_is_list, jsx_is_list_item, jsx_name,
+ jsx_props_to_obj, jsx_text_to_str, transform_jsx_attr_str,
+ },
+ target::TransformTarget,
+ transform_mode::TransformMode,
+ utils::{calc_hash, calc_hash_number},
+};
+
+use self::attr_name::AttrName;
+
+// impl From for Expr {
+// fn from(value: i32) -> Self {
+// Expr::Lit(Lit::Num(Number {
+// span: DUMMY_SP,
+// value: value as f64,
+// raw: None,
+// }))
+// }
+// }
+
+static WRAPPER_NODE: Lazy = Lazy::new(|| JSXElement {
+ span: DUMMY_SP,
+ opening: JSXOpeningElement {
+ span: DUMMY_SP,
+ name: JSXElementName::Ident(Ident::new(
+ "wrapper".into(),
+ DUMMY_SP,
+ SyntaxContext::default(),
+ )),
+ attrs: vec![],
+ self_closing: true,
+ type_args: None,
+ },
+ closing: None,
+ children: vec![],
+});
+
+static WRAPPER_NODE_2: Lazy = Lazy::new(|| JSXElement {
+ span: DUMMY_SP,
+ opening: JSXOpeningElement {
+ span: DUMMY_SP,
+ name: JSXElementName::Ident(Ident::new(
+ "wrapper".into(),
+ DUMMY_SP,
+ SyntaxContext::default(),
+ )),
+ attrs: vec![],
+ self_closing: false,
+ type_args: None,
+ },
+ closing: Some(JSXClosingElement {
+ span: DUMMY_SP,
+ name: JSXElementName::Ident(Ident::new(
+ "wrapper".into(),
+ DUMMY_SP,
+ SyntaxContext::default(),
+ )),
+ }),
+ children: vec![],
+});
+
+static NO_FLATTEN_ATTRIBUTES: Lazy> = Lazy::new(|| {
+ HashSet::from([
+ "name".to_string(),
+ "clip-radius".to_string(),
+ "overlap".to_string(),
+ "exposure-scene".to_string(),
+ "exposure-id".to_string(),
+ ])
+});
+
+#[derive(Debug)]
+pub enum DynamicPart {
+ Attr(Expr, i32, AttrName),
+ Spread(Expr, i32),
+ Slot(Expr, i32),
+ ListSlot(Expr, i32),
+}
+
+pub fn i32_to_expr(i: &i32) -> Expr {
+ Expr::Lit(Lit::Num(Number {
+ span: DUMMY_SP,
+ value: *i as f64,
+ raw: None,
+ }))
+}
+
+fn bool_jsx_attr(value: bool) -> JSXAttrValue {
+ JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ span: DUMMY_SP,
+ expr: JSXExpr::Expr(Box::new(Expr::Lit(Lit::Bool(Bool {
+ span: DUMMY_SP,
+ value,
+ })))),
+ })
+}
+
+impl DynamicPart {
+ fn to_updater(&self, runtime_id: Expr, target: TransformTarget, exp_index: i32) -> Expr {
+ match target {
+ TransformTarget::LEPUS | TransformTarget::MIXED => match self {
+ DynamicPart::Attr(_, element_index, attr_name) => match attr_name {
+ AttrName::Attr(name) => quote!(
+ "function (ctx) {
+ if (ctx.__elements) {
+ __SetAttribute(ctx.__elements[$element_index], $name, ctx.__values[$exp_index]);
+ }
+ }" as Expr,
+ name: Expr = Expr::Lit(Lit::Str(name.clone().into())),
+ element_index: Expr = i32_to_expr(element_index),
+ exp_index: Expr = i32_to_expr(&exp_index),
+ ),
+ AttrName::TimingFlag => quote!(
+ "function (ctx) {
+ if (ctx.__elements) {
+ __SetAttribute(ctx.__elements[$element_index], '__lynx_timing_flag', ctx.__values[$exp_index].__ltf);
+ }
+ }" as Expr,
+ element_index: Expr = i32_to_expr(element_index),
+ exp_index: Expr = i32_to_expr(&exp_index),
+ ),
+ AttrName::Dataset(name) => quote!(
+ "function (ctx) {
+ if (ctx.__elements) {
+ __AddDataset(ctx.__elements[$element_index], $name, ctx.__values[$exp_index]);
+ }
+ }" as Expr,
+ name: Expr = Expr::Lit(Lit::Str(name.clone().into())),
+ element_index: Expr = i32_to_expr(element_index),
+ exp_index: Expr = i32_to_expr(&exp_index),
+ ),
+ AttrName::Style => quote!(
+ "function (ctx) {
+ if (ctx.__elements) {
+ __SetInlineStyles(ctx.__elements[$element_index], ctx.__values[$exp_index]);
+ }
+ }" as Expr,
+ element_index: Expr = i32_to_expr(element_index),
+ exp_index: Expr = i32_to_expr(&exp_index),
+ ),
+ AttrName::Class => quote!(
+ "function (ctx) {
+ if (ctx.__elements) {
+ __SetClasses(ctx.__elements[$element_index], ctx.__values[$exp_index] || '');
+ }
+ }" as Expr,
+ element_index: Expr = i32_to_expr(element_index),
+ exp_index: Expr = i32_to_expr(&exp_index),
+ ),
+ AttrName::ID => quote!(
+ "function (ctx) {
+ if (ctx.__elements) {
+ __SetID(ctx.__elements[$element_index], ctx.__values[$exp_index]);
+ }
+ }" as Expr,
+ element_index: Expr = i32_to_expr(element_index),
+ exp_index: Expr = i32_to_expr(&exp_index),
+ ),
+ AttrName::Event(event_type, event_name) => quote!(
+ "(snapshot, index, oldValue) => $runtime_id.updateEvent(snapshot, index, oldValue, $element_index, $event_type, $event_name, '')" as Expr,
+ runtime_id: Expr = runtime_id.clone(),
+ event_type: Expr = Expr::Lit(Lit::Str(event_type.clone().into())),
+ event_name: Expr = Expr::Lit(Lit::Str(event_name.clone().into())),
+ element_index: Expr = i32_to_expr(element_index),
+ ),
+ AttrName::WorkletEvent(worklet_type, event_type, event_name) => quote!(
+ "(snapshot, index, oldValue) => $runtime_id.updateWorkletEvent(snapshot, index, oldValue, $element_index, $worklet_type, $event_type, $event_name)" as Expr,
+ runtime_id: Expr = runtime_id.clone(),
+ worklet_type: Expr = Expr::Lit(Lit::Str(worklet_type.clone().into())),
+ event_type: Expr = Expr::Lit(Lit::Str(event_type.clone().into())),
+ event_name: Expr = Expr::Lit(Lit::Str(event_name.clone().into())),
+ element_index: Expr = i32_to_expr(element_index),
+ ),
+ AttrName::Ref => quote!(
+ "(snapshot, index, oldValue) => $runtime_id.updateRef(snapshot, index, oldValue, $element_index)" as Expr,
+ runtime_id: Expr = runtime_id.clone(),
+ element_index: Expr = i32_to_expr(element_index),
+ ),
+ AttrName::WorkletRef(worklet_type) => quote!(
+ "(snapshot, index, oldValue) => $runtime_id.updateWorkletRef(snapshot, index, oldValue, $element_index, $worklet_type)" as Expr,
+ runtime_id: Expr = runtime_id.clone(),
+ element_index: Expr = i32_to_expr(element_index),
+ worklet_type: Expr = Expr::Lit(Lit::Str(worklet_type.clone().into())),
+ ),
+ AttrName::ListItemPlatformInfo => quote!(
+ "(snapshot, index, oldValue) => $runtime_id.updateListItemPlatformInfo(snapshot, index, oldValue, $element_index)" as Expr,
+ runtime_id: Expr = runtime_id.clone(),
+ element_index: Expr = i32_to_expr(element_index),
+ ),
+ AttrName::Gesture(ns) => quote!(
+ "(snapshot, index, oldValue) => $runtime_id.updateGesture(snapshot, index, oldValue, $element_index, $ns)" as Expr,
+ runtime_id: Expr = runtime_id.clone(),
+ element_index: Expr = i32_to_expr(element_index),
+ ns: Expr = Expr::Lit(Lit::Str(ns.clone().into())),
+ ),
+ },
+ DynamicPart::Spread(_, element_index) => quote!(
+ "(snapshot, index, oldValue) => $runtime_id.updateSpread(snapshot, index, oldValue, $element_index)" as Expr,
+ runtime_id: Expr = runtime_id.clone(),
+ element_index: Expr = i32_to_expr(element_index)
+ ),
+ DynamicPart::Slot(_, _) => Expr::Lit(Lit::Null(Null { span: DUMMY_SP })),
+ DynamicPart::ListSlot(_, _) => Expr::Lit(Lit::Null(Null { span: DUMMY_SP })),
+ },
+ TransformTarget::JS => Expr::Lit(Lit::Null(Null { span: DUMMY_SP })),
+ }
+ }
+}
+
+pub struct DynamicPartExtractor<'a, V, F>
+where
+ V: VisitMut,
+ F: Fn(Span) -> Expr,
+{
+ page_id: Lazy,
+ runtime_id: Expr,
+ parent_element: Option,
+ element_index: i32,
+ element_ids: HashMap,
+ static_stmts: Vec>,
+ si_id: Lazy,
+ snapshot_creator: Option,
+ dynamic_parts: Vec,
+ dynamic_part_visitor: &'a mut V,
+ key: Option,
+ attr_slot_counter: i32,
+ element_slot_counter: i32,
+ enable_element_template: bool,
+ enable_ui_source_map: bool,
+ node_index_fn: F,
+}
+
+impl<'a, V, F> DynamicPartExtractor<'a, V, F>
+where
+ V: VisitMut,
+ F: Fn(Span) -> Expr,
+{
+ fn new(
+ runtime_id: Expr,
+ dynamic_part_visitor: &'a mut V,
+ enable_element_template: bool,
+ enable_ui_source_map: bool,
+ node_index_fn: F,
+ ) -> Self {
+ DynamicPartExtractor {
+ page_id: Lazy::new(|| private_ident!("pageId")),
+ runtime_id,
+ parent_element: None,
+ element_index: 0,
+ element_ids: HashMap::new(),
+ static_stmts: vec![],
+ si_id: Lazy::new(|| private_ident!("snapshotInstance")),
+ snapshot_creator: None,
+ dynamic_parts: vec![],
+ dynamic_part_visitor,
+ key: None,
+ attr_slot_counter: 0,
+ element_slot_counter: 0,
+ enable_element_template,
+ enable_ui_source_map,
+ node_index_fn,
+ }
+ }
+
+ fn node_index_expr_from_span(&self, span: Span) -> Expr {
+ (self.node_index_fn)(span)
+ }
+
+ fn node_index_config_expr(&self, span: Span) -> Expr {
+ Expr::Object(ObjectLit {
+ span: DUMMY_SP,
+ props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
+ key: PropName::Ident(IdentName::new("nodeIndex".into(), DUMMY_SP)),
+ value: Box::new(self.node_index_expr_from_span(span)),
+ })))],
+ })
+ }
+
+ fn next_attr_slot_index(&mut self) -> i32 {
+ let idx = self.attr_slot_counter;
+ self.attr_slot_counter += 1;
+ idx
+ }
+
+ fn next_element_slot_index(&mut self) -> i32 {
+ let idx = self.element_slot_counter;
+ self.element_slot_counter += 1;
+ idx
+ }
+
+ fn push_dynamic_attr(&mut self, value: Expr, attr_name: AttrName) {
+ // Snapshot updaters address dynamic attrs by element index. ET descriptors
+ // address the compact attribute slot array, so the two modes must keep
+ // separate counters even though they share the same extractor.
+ let index = if self.enable_element_template {
+ self.next_attr_slot_index()
+ } else {
+ self.element_index
+ };
+ self
+ .dynamic_parts
+ .push(DynamicPart::Attr(value, index, attr_name));
+ }
+
+ fn push_dynamic_spread(&mut self, value: Expr) {
+ let index = if self.enable_element_template {
+ self.next_attr_slot_index()
+ } else {
+ self.element_index
+ };
+ self.dynamic_parts.push(DynamicPart::Spread(value, index));
+ }
+
+ fn next_children_slot_index(&mut self) -> i32 {
+ if self.enable_element_template {
+ self.next_element_slot_index()
+ } else {
+ self.element_index
+ }
+ }
+
+ fn static_stmt_from_jsx_element(&mut self, n: &JSXElement, el: Ident) -> Stmt {
+ let mut static_stmt: Stmt = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
+
+ if let Expr::Lit(Lit::Str(str)) = *jsx_name(n.opening.name.clone()) {
+ let tag = str.value.to_string_lossy();
+ match tag.as_ref() {
+ "view" => {
+ static_stmt = quote!(
+ r#"const $element = __CreateView($page_id)"# as Stmt,
+ element = el.clone(),
+ page_id = self.page_id.clone(),
+ );
+ }
+ "scroll-view" => {
+ static_stmt = quote!(
+ r#"const $element = __CreateScrollView($page_id)"# as Stmt,
+ element = el.clone(),
+ page_id = self.page_id.clone(),
+ );
+ }
+ "x-scroll-view" => {
+ static_stmt = quote!(
+ r#"const $element = __CreateScrollView($page_id, { tag: "x-scroll-view" })"# as Stmt,
+ element = el.clone(),
+ page_id = self.page_id.clone(),
+ );
+ }
+ "image" => {
+ static_stmt = quote!(
+ r#"const $element = __CreateImage($page_id)"# as Stmt,
+ element = el.clone(),
+ page_id = self.page_id.clone(),
+ );
+ }
+ "text" => {
+ static_stmt = quote!(
+ r#"const $element = __CreateText($page_id)"# as Stmt,
+ element = el.clone(),
+ page_id = self.page_id.clone(),
+ );
+ }
+ "wrapper" => {
+ static_stmt = quote!(
+ r#"const $element = __CreateWrapperElement($page_id)"# as Stmt,
+ element = el.clone(),
+ page_id = self.page_id.clone(),
+ );
+ }
+ "list" => {
+ static_stmt = quote!(
+ r#"const $element = $runtime_id.snapshotCreateList($page_id, $si_id, $element_index)"#
+ as Stmt,
+ element = el.clone(),
+ runtime_id: Expr = self.runtime_id.clone(),
+ page_id = self.page_id.clone(),
+ si_id = self.si_id.clone(),
+ element_index: Expr = Expr::Lit(Lit::Num(Number { span: DUMMY_SP, value: self.element_index as f64, raw: None })),
+ );
+ }
+ "frame" => {
+ static_stmt = quote!(
+ r#"const $element = __CreateFrame($page_id)"# as Stmt,
+ element = el.clone(),
+ page_id = self.page_id.clone(),
+ );
+ }
+ _ => {
+ static_stmt = quote!(
+ r#"const $element = __CreateElement($name, $page_id)"# as Stmt,
+ element = el.clone(),
+ name: Expr = Expr::Lit(Lit::Str(str)),
+ page_id = self.page_id.clone(),
+ );
+ }
+ };
+ }
+
+ if self.enable_ui_source_map {
+ let node_index_expr = self.node_index_config_expr(n.span);
+ if let Stmt::Decl(Decl::Var(var_decl)) = &mut static_stmt {
+ if let Some(VarDeclarator {
+ init: Some(init), ..
+ }) = var_decl.decls.get_mut(0)
+ {
+ if let Expr::Call(call_expr) = init.as_mut() {
+ call_expr.args.push(ExprOrSpread {
+ spread: None,
+ expr: Box::new(node_index_expr),
+ });
+ }
+ }
+ }
+ }
+
+ static_stmt
+ }
+}
+
+impl VisitMut for DynamicPartExtractor<'_, V, F>
+where
+ V: VisitMut,
+ F: Fn(Span) -> Expr,
+{
+ fn visit_mut_jsx_element_childs(&mut self, n: &mut Vec) {
+ if n.is_empty() {
+ return;
+ }
+
+ // merge dynamic parts together to reduce wrapper node count
+
+ let mut merged_children: Vec = vec![];
+ let mut current_chunk: Vec = vec![];
+
+ for mut child in n.take() {
+ let should_merge: bool;
+ match child {
+ JSXElementChild::JSXText(ref text) => {
+ if jsx_text_to_str(&text.value).is_empty() {
+ should_merge = current_chunk.is_empty();
+ } else {
+ should_merge = true;
+ }
+ }
+ JSXElementChild::JSXElement(ref element) => {
+ should_merge = !jsx_is_custom(element);
+ }
+ JSXElementChild::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(ref _expr),
+ ..
+ }) => {
+ should_merge = false;
+ }
+ JSXElementChild::JSXFragment(_)
+ | JSXElementChild::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::JSXEmptyExpr(_),
+ ..
+ }) => {
+ should_merge = true;
+ }
+ JSXElementChild::JSXSpreadChild(_) => {
+ unreachable!("JSXSpreadChild is not supported yet");
+ }
+ }
+
+ if should_merge {
+ if !current_chunk.is_empty() {
+ current_chunk.visit_mut_with(self.dynamic_part_visitor);
+ let slot_index = self.next_children_slot_index();
+ self.dynamic_parts.push(DynamicPart::Slot(
+ jsx_children_to_expr(current_chunk.take()),
+ slot_index,
+ ));
+
+ let mut child = JSXElementChild::JSXElement(Box::new(WRAPPER_NODE_2.clone()));
+ child.visit_mut_with(self);
+ merged_children.push(child);
+ }
+
+ child.visit_mut_with(self);
+ merged_children.push(child);
+ } else {
+ current_chunk.push(child);
+ }
+ }
+
+ if !current_chunk.is_empty() {
+ current_chunk.visit_mut_with(self.dynamic_part_visitor);
+ let slot_index = self.next_children_slot_index();
+ self.dynamic_parts.push(DynamicPart::Slot(
+ jsx_children_to_expr(current_chunk.take()),
+ slot_index,
+ ));
+
+ let mut child = JSXElementChild::JSXElement(Box::new(WRAPPER_NODE_2.clone()));
+ child.visit_mut_with(self);
+ merged_children.push(child);
+ }
+
+ *n = merged_children;
+ }
+
+ fn visit_mut_jsx_element(&mut self, n: &mut JSXElement) {
+ if self.enable_element_template && self.parent_element.is_some() && jsx_is_list(n) {
+ n.visit_mut_with(self.dynamic_part_visitor);
+
+ let element_slot_index = self.next_children_slot_index();
+ self.dynamic_parts.push(DynamicPart::ListSlot(
+ Expr::JSXElement(Box::new(n.take())),
+ element_slot_index,
+ ));
+
+ *n = WRAPPER_NODE.clone();
+ n.visit_mut_with(self);
+ return;
+ }
+
+ if !jsx_is_custom(n) {
+ match Lazy::::get(&self.page_id) {
+ Some(_) => {}
+ None => {
+ self.static_stmts.push(RefCell::new(quote!(
+ r#"const $page_id = $runtime_id.__pageId"# as Stmt,
+ page_id = self.page_id.clone(),
+ runtime_id: Expr = self.runtime_id.clone(),
+ )));
+ }
+ }
+
+ let el = private_ident!("el");
+ self.element_ids.insert(self.element_index, el.clone());
+
+ if (jsx_has_dynamic_key(n)) && self.parent_element.is_some() {
+ let is_list = jsx_is_list(n);
+ n.visit_mut_with(self.dynamic_part_visitor);
+ let expr = Expr::JSXElement(Box::new(n.take()));
+ let slot_index = self.next_children_slot_index();
+
+ if is_list {
+ self
+ .dynamic_parts
+ .push(DynamicPart::ListSlot(expr, slot_index));
+ } else {
+ self.dynamic_parts.push(DynamicPart::Slot(expr, slot_index));
+ }
+
+ *n = WRAPPER_NODE_2.clone();
+ }
+
+ let static_stmt = self.static_stmt_from_jsx_element(n, el.clone());
+ let static_stmt = RefCell::new(static_stmt);
+ self.static_stmts.push(static_stmt.clone());
+
+ {
+ let mut flatten = None;
+ for attr in &n.opening.attrs {
+ if let JSXAttrOrSpread::JSXAttr(attr) = attr {
+ let name = jsx_attr_name(&attr.name.clone()).to_string();
+ if NO_FLATTEN_ATTRIBUTES.contains(&name) {
+ flatten = Some(JSXAttrOrSpread::JSXAttr(JSXAttr {
+ span: DUMMY_SP,
+ name: JSXAttrName::Ident(IdentName::new("flatten".into(), DUMMY_SP)),
+ value: Some(bool_jsx_attr(false)),
+ }));
+ break;
+ }
+ }
+ }
+
+ if let Some(flatten) = flatten {
+ let mut has_origin_flatten = false;
+ for attr in &mut n.opening.attrs {
+ if let JSXAttrOrSpread::JSXAttr(attr) = attr {
+ let name = jsx_attr_name(&attr.name.clone()).to_string();
+ if name == *"flatten" {
+ attr.value = Some(bool_jsx_attr(false));
+ has_origin_flatten = true;
+ }
+ }
+ }
+ if !has_origin_flatten {
+ n.opening.attrs.push(flatten);
+ }
+ }
+ }
+
+ let has_spread_element = n
+ .opening
+ .attrs
+ .iter()
+ .any(|attr_or_spread| match attr_or_spread {
+ JSXAttrOrSpread::SpreadElement(_) => true,
+ JSXAttrOrSpread::JSXAttr(_) => false,
+ });
+
+ // Snapshot bundles list-item platform attrs into a hidden updater value.
+ // ET keeps them as normal template attrs; consuming them here would create
+ // hidden slots before the ET list path can read the descriptors directly.
+ if jsx_is_list_item(n) && !self.enable_element_template {
+ if has_spread_element {
+ } else {
+ let mut list_item_platform_info: Vec = vec![];
+ n.opening.attrs.retain_mut(|attr_or_spread| {
+ match attr_or_spread {
+ JSXAttrOrSpread::JSXAttr(attr) => {
+ if let JSXAttrName::Ident(id) = &attr.name {
+ match id.sym.to_string().as_str() {
+ "reuse-identifier"
+ | "full-span"
+ | "item-key"
+ | "sticky-top"
+ | "sticky-bottom"
+ | "estimated-height"
+ | "estimated-height-px"
+ | "estimated-main-axis-size-px"
+ | "recyclable" => {
+ list_item_platform_info.push(attr.clone());
+ return false;
+ }
+ &_ => {}
+ }
+ }
+ }
+ JSXAttrOrSpread::SpreadElement(_spread) => {
+ return false;
+ }
+ }
+
+ true
+ });
+ if !list_item_platform_info.is_empty() {
+ self.push_dynamic_attr(
+ Expr::Object(ObjectLit {
+ span: DUMMY_SP,
+ props: list_item_platform_info
+ .iter()
+ .map(jsx_attr_to_prop)
+ .collect(),
+ }),
+ AttrName::ListItemPlatformInfo,
+ );
+ }
+ }
+ }
+
+ // pick key from n.opening.attrs
+ n.opening
+ .attrs
+ .retain_mut(|attr_or_spread| match attr_or_spread {
+ JSXAttrOrSpread::SpreadElement(_) => true,
+ JSXAttrOrSpread::JSXAttr(JSXAttr { name, value, .. }) => match name {
+ JSXAttrName::Ident(ident_name) => match ident_name.sym.as_ref() {
+ "key" => {
+ if self.parent_element.is_none() {
+ self.key = value.take();
+ }
+ false
+ }
+ _ => true,
+ },
+ JSXAttrName::JSXNamespacedName(_) => true,
+ },
+ });
+
+ if has_spread_element {
+ if self.enable_element_template {
+ for attr_or_spread in &mut n.opening.attrs {
+ match attr_or_spread {
+ JSXAttrOrSpread::SpreadElement(spread) => {
+ self.push_dynamic_spread(*spread.expr.clone());
+ }
+ JSXAttrOrSpread::JSXAttr(JSXAttr { name, value, .. }) => match name {
+ JSXAttrName::Ident(ident_name) => {
+ let attr_name =
+ AttrName::from(>::into(ident_name.clone()));
+ match &attr_name {
+ AttrName::Attr(_) | AttrName::Dataset(_) | AttrName::Class | AttrName::ID => {
+ if let Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ ..
+ })) = value
+ {
+ if !matches!(&**expr, Expr::Lit(_)) {
+ self.push_dynamic_attr(*expr.clone(), attr_name.clone());
+ }
+ }
+ }
+ AttrName::Style => {
+ let mut static_style_val = None;
+ if let Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ span,
+ ..
+ })) = value
+ {
+ let expr = &**expr;
+ if is_literal(expr) {
+ if let Some(s) = get_string_inline_style_from_literal(expr, span) {
+ static_style_val = Some((s, *span));
+ }
+ }
+ }
+
+ if let Some((s_val, span)) = static_style_val {
+ *value = Some(JSXAttrValue::Str(Str {
+ span,
+ value: s_val.into(),
+ raw: None,
+ }));
+ } else if let Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ ..
+ })) = value
+ {
+ self.push_dynamic_attr(*expr.clone(), attr_name.clone());
+ }
+ }
+ AttrName::Event(..) | AttrName::Ref => {
+ self.push_dynamic_attr(*jsx_attr_value((*value).clone()), attr_name.clone());
+ }
+ AttrName::TimingFlag => {
+ self.push_dynamic_attr(
+ *quote_expr!("{__ltf: $flag}", flag: Expr = *jsx_attr_value((*value).clone())),
+ attr_name.clone(),
+ );
+ }
+ AttrName::ListItemPlatformInfo => unreachable!(
+ "Unexpected ListItemPlatformInfo attribute in static JSX processing"
+ ),
+ AttrName::WorkletEvent(..) | AttrName::WorkletRef(..) => {
+ unreachable!("A worklet event should have an attribute namespace.")
+ }
+ AttrName::Gesture(..) => {
+ unreachable!("A gesture should have an attribute namespace.")
+ }
+ }
+ }
+ JSXAttrName::JSXNamespacedName(JSXNamespacedName { ns, name, .. }) => {
+ let attr_name: AttrName =
+ AttrName::from_ns(ns.clone().into(), name.clone().into());
+ match attr_name {
+ AttrName::WorkletEvent(..)
+ | AttrName::WorkletRef(..)
+ | AttrName::Gesture(..) => {
+ self.push_dynamic_attr(*jsx_attr_value((*value).clone()), attr_name.clone());
+ }
+ _ => todo!(),
+ }
+ }
+ },
+ }
+ }
+ } else {
+ // TODO: avoid clone
+ let mut spread_obj = jsx_props_to_obj(n).unwrap();
+ spread_obj.props.push(
+ Prop::KeyValue(KeyValueProp {
+ key: PropName::Ident(IdentName::new("__spread".into(), DUMMY_SP)),
+ value: Expr::Lit(Lit::Bool(true.into())).into(),
+ })
+ .into(),
+ );
+ self.push_dynamic_spread(Expr::Object(spread_obj));
+ }
+ } else {
+ let el = Expr::Ident(el.clone());
+
+ n.opening
+ .attrs
+ .iter_mut()
+ .for_each(|attr_or_spread| match attr_or_spread {
+ JSXAttrOrSpread::SpreadElement(_) => todo!(),
+ JSXAttrOrSpread::JSXAttr(JSXAttr { name, value, .. }) => {
+ match name {
+ JSXAttrName::Ident(ident_name) => {
+ let attr_name = AttrName::from(>::into(ident_name.clone()));
+ match &attr_name {
+ AttrName::Attr(name) => {
+ match value {
+ None => {
+ let stmt = quote!(
+ r#"__SetAttribute($element, $name, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ name: Expr = name.clone().into(),
+ value: Expr = Expr::Lit(Lit::Bool(Bool {span: DUMMY_SP, value: true}))
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ Some(JSXAttrValue::Str(s)) => {
+ let value = transform_jsx_attr_str(&s.value);
+ let stmt = quote!(
+ r#"__SetAttribute($element, $name, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ name: Expr = name.clone().into(),
+ value: Expr = Expr::Lit(Lit::Str(Str { span: s.span, value: value.into(), raw: None }))
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ ..
+ })) => {
+ // expr.map_with_mut(|value| {
+ // value.fold_with(self.dynamic_part_visitor)
+ // });
+ match &**expr {
+ Expr::Lit(value) => {
+ let stmt = quote!(
+ r#"__SetAttribute($element, $name, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ name: Expr = name.clone().into(),
+ value: Expr = Expr::Lit(value.clone())
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ _ => {
+ self.push_dynamic_attr(*expr.clone(), attr_name.clone());
+ }
+ }
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::JSXEmptyExpr(_),
+ ..
+ })) => {}
+ Some(JSXAttrValue::JSXElement(_)) => unreachable!("Unexpected JSXElement in JSX attribute value - not supported"),
+ Some(JSXAttrValue::JSXFragment(_)) => unreachable!("Unexpected JSXFragment in JSX attribute value - not supported"),
+ };
+ }
+ AttrName::Dataset(name) => {
+ match value {
+ None => {
+ let stmt = quote!(
+ r#"__AddDataset($element, $name, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ name: Expr = name.clone().into(),
+ value: Expr = Expr::Lit(Lit::Bool(Bool {span: DUMMY_SP, value: true}))
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ Some(JSXAttrValue::Str(s)) => {
+ let value = transform_jsx_attr_str(&s.value);
+ let stmt = quote!(
+ r#"__AddDataset($element, $name, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ name: Expr = name.clone().into(),
+ value: Expr = Expr::Lit(Lit::Str(Str { span: s.span, value: value.into(), raw: None }))
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ ..
+ })) => {
+ if !(self.enable_element_template && matches!(&**expr, Expr::Lit(_))) {
+ self.push_dynamic_attr(*expr.clone(), attr_name.clone());
+ }
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::JSXEmptyExpr(_),
+ ..
+ })) => {}
+ Some(JSXAttrValue::JSXElement(_)) => unreachable!("Unexpected JSXElement in JSX attribute value - not supported"),
+ Some(JSXAttrValue::JSXFragment(_)) => unreachable!("Unexpected JSXFragment in JSX attribute value - not supported"),
+ };
+ }
+ AttrName::Event(..) | AttrName::Ref => {
+ self.push_dynamic_attr(*jsx_attr_value((*value).clone()), attr_name.clone());
+ }
+ AttrName::TimingFlag => {
+ self.push_dynamic_attr(
+ *quote_expr!("{__ltf: $flag}", flag: Expr = *jsx_attr_value((*value).clone())),
+ attr_name.clone(),
+ );
+ }
+ AttrName::Style => {
+ let mut static_style_val = None;
+ if let Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ span,
+ ..
+ })) = value
+ {
+ let expr = &**expr;
+ if is_literal(expr) {
+ if let Some(s) = get_string_inline_style_from_literal(expr, span) {
+ static_style_val = Some((s, *span));
+ }
+ }
+ }
+
+ if let Some((s_val, span)) = static_style_val {
+ if self.enable_element_template {
+ *value = Some(JSXAttrValue::Str(Str {
+ span,
+ value: s_val.into(),
+ raw: None,
+ }));
+ } else {
+ // ;
+ // ;
+ let s = Lit::Str(Str {
+ span,
+ value: s_val.into(),
+ raw: None,
+ });
+ let stmt = quote!(
+ r#"__SetInlineStyles($element, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ value: Expr = Expr::Lit(s)
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ } else {
+ match value {
+ None => {}
+ Some(JSXAttrValue::Str(s)) => {
+ // ;
+ let value = transform_jsx_attr_str(&s.value);
+ let stmt = quote!(
+ r#"__SetInlineStyles($element, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ value: Expr = Expr::Lit(Lit::Str(Str { span: s.span, value: value.into(), raw: None }))
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ ..
+ })) => {
+ self.push_dynamic_attr(*expr.clone(), attr_name.clone());
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::JSXEmptyExpr(_),
+ ..
+ })) => {}
+ Some(JSXAttrValue::JSXElement(_)) => unreachable!("Unexpected JSXElement in JSX attribute value - not supported"),
+ Some(JSXAttrValue::JSXFragment(_)) => unreachable!("Unexpected JSXFragment in JSX attribute value - not supported"),
+ }
+ }
+ }
+ AttrName::Class => {
+ match value {
+ None => {}
+ Some(JSXAttrValue::Str(s)) => {
+ let value = transform_jsx_attr_str(&s.value);
+ let stmt = quote!(
+ r#"__SetClasses($element, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ value: Expr = Expr::Lit(Lit::Str(Str { span: s.span, value: value.into(), raw: None }))
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ ..
+ })) => match &**expr {
+ Expr::Lit(value) => {
+ let stmt = quote!(
+ r#"__SetClasses($element, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ value: Expr = Expr::Lit(value.clone())
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ _ => {
+ self.push_dynamic_attr(*expr.clone(), attr_name.clone());
+ }
+ },
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::JSXEmptyExpr(_),
+ ..
+ })) => {}
+ Some(JSXAttrValue::JSXElement(_)) => unreachable!("Unexpected JSXElement in JSX attribute value - not supported"),
+ Some(JSXAttrValue::JSXFragment(_)) => unreachable!("Unexpected JSXFragment in JSX attribute value - not supported"),
+ };
+ }
+ AttrName::ID => {
+ match value {
+ None => {}
+ Some(JSXAttrValue::Str(s)) => {
+ let value = transform_jsx_attr_str(&s.value);
+ let stmt = quote!(
+ r#"__SetID($element, $value)"# as Stmt,
+ element: Expr = el.clone(),
+ value: Expr = Expr::Lit(Lit::Str(Str { span: s.span, value: value.into(), raw: None }))
+ );
+ self.static_stmts.push(RefCell::new(stmt));
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::Expr(expr),
+ ..
+ })) => {
+ if !(self.enable_element_template && matches!(&**expr, Expr::Lit(_))) {
+ self.push_dynamic_attr(*expr.clone(), attr_name);
+ }
+ }
+ Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
+ expr: JSXExpr::JSXEmptyExpr(_),
+ ..
+ })) => {}
+ Some(JSXAttrValue::JSXElement(_)) => unreachable!("Unexpected JSXElement in JSX attribute value - not supported"),
+ Some(JSXAttrValue::JSXFragment(_)) => unreachable!("Unexpected JSXFragment in JSX attribute value - not supported"),
+ };
+ }
+ AttrName::ListItemPlatformInfo => unreachable!("Unexpected ListItemPlatformInfo attribute in static JSX processing"),
+ AttrName::WorkletEvent(..) | AttrName::WorkletRef(..) => {
+ unreachable!("A worklet event should have an attribute namespace.")
+ }
+ AttrName::Gesture(..) => {
+ unreachable!("A gesture should have an attribute namespace.")
+ }
+ }
+ }
+ JSXAttrName::JSXNamespacedName(JSXNamespacedName { ns, name, .. }) => {
+ let attr_name: AttrName = AttrName::from_ns(ns.clone().into(), name.clone().into());
+ match attr_name {
+ AttrName::WorkletEvent(..) | AttrName::WorkletRef(..) => {
+ self.push_dynamic_attr(*jsx_attr_value((*value).clone()), attr_name.clone());
+ }
+ AttrName::Gesture(..) => {
+ self.push_dynamic_attr(*jsx_attr_value((*value).clone()), attr_name.clone());
+ }
+ _ => todo!(),
+ }
+ }
+ };
+ }
+ });
+ }
+
+ if let Some(parent_el) = &self.parent_element {
+ self.static_stmts.push(RefCell::new(quote!(
+ r#"__AppendElement($parent, $child)"# as Stmt,
+ parent: Ident = parent_el.clone(),
+ child: Ident = el.clone(),
+ )));
+ };
+
+ let is_children_full_dynamic = jsx_is_children_full_dynamic(n);
+
+ if !is_children_full_dynamic {
+ self.element_index += 1;
+
+ let pre_parent_element = self.parent_element.take();
+ self.parent_element = Some(el.clone());
+ n.visit_mut_children_with(self);
+ self.parent_element = pre_parent_element;
+ } else {
+ n.visit_mut_children_with(self.dynamic_part_visitor);
+ let children_expr = jsx_children_to_expr(n.children.take());
+ let slot_index = self.next_children_slot_index();
+ if jsx_is_list(n) {
+ self
+ .dynamic_parts
+ .push(DynamicPart::ListSlot(children_expr, slot_index));
+ } else {
+ self
+ .dynamic_parts
+ .push(DynamicPart::Slot(children_expr, slot_index));
+ }
+
+ if self.enable_element_template {
+ n.children = vec![JSXElementChild::JSXElement(Box::new(WRAPPER_NODE.clone()))];
+ }
+
+ self.element_index += 1;
+ }
+
+ if self.parent_element.is_none() {
+ let elements = Expr::Array(ArrayLit {
+ span: DUMMY_SP,
+ elems: (0..self.element_ids.len())
+ .step_by(1)
+ .map(|e| e as i32)
+ .map(|e| {
+ Some(ExprOrSpread {
+ spread: None,
+ expr: Box::new(Expr::Ident(self.element_ids[&e].clone())),
+ })
+ })
+ .collect(),
+ });
+
+ self.static_stmts.push(RefCell::new(quote!(
+ r#"return $elements;"# as Stmt,
+ elements: Expr = elements,
+ )));
+
+ self.snapshot_creator = Some(Function {
+ ctxt: SyntaxContext::default(),
+ params: match Lazy::::get(&self.si_id) {
+ Some(_) => vec![Param {
+ span: DUMMY_SP,
+ decorators: vec![],
+ pat: Pat::Ident(BindingIdent {
+ id: self.si_id.take(),
+ type_ann: None,
+ }),
+ }],
+ None => vec![],
+ },
+ decorators: vec![],
+ span: DUMMY_SP,
+ body: Some(BlockStmt {
+ ctxt: SyntaxContext::default(),
+ span: DUMMY_SP,
+ stmts: self
+ .static_stmts
+ .take()
+ .into_iter()
+ .map(|mut stmt| stmt.get_mut().take())
+ .collect(),
+ }),
+ is_generator: false,
+ is_async: false,
+ type_params: None,
+ return_type: None,
+ });
+ };
+ } else {
+ n.visit_mut_children_with(self.dynamic_part_visitor);
+
+ if self.parent_element.is_some() {
+ let element_slot_index = self.next_children_slot_index();
+ self.dynamic_parts.push(DynamicPart::Slot(
+ Expr::JSXElement(Box::new(n.take())),
+ element_slot_index,
+ ));
+
+ // self.element_index += 1;
+ *n = WRAPPER_NODE.clone();
+ n.visit_mut_with(self);
+ }
+ }
+ }
+
+ fn visit_mut_jsx_text(&mut self, n: &mut JSXText) {
+ let t = jsx_text_to_str(&n.value);
+
+ if !t.is_empty() {
+ let el = private_ident!("el");
+ self.element_ids.insert(self.element_index, el.clone());
+
+ self.static_stmts.push(RefCell::new(quote!(
+ r#"const $element = __CreateRawText($t)"# as Stmt,
+ element = el.clone(),
+ t: Expr = t.into(),
+ )));
+
+ if let Some(parent_el) = &self.parent_element {
+ self.static_stmts.push(RefCell::new(quote!(
+ r#"__AppendElement($parent, $child)"# as Stmt,
+ parent: Ident = parent_el.clone(),
+ child: Ident = el.clone(),
+ )));
+ };
+
+ self.element_index += 1;
+ }
+ }
+}
+
+/// @internal
+#[derive(Deserialize, PartialEq, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct JSXTransformerConfig {
+ /// @internal
+ pub preserve_jsx: bool,
+ /// @internal
+ pub runtime_pkg: String,
+ /// @internal
+ pub jsx_import_source: Option,
+ /// @internal
+ pub filename: String,
+ /// @internal
+ pub target: TransformTarget,
+ /// @internal
+ #[serde(default)]
+ pub enable_ui_source_map: bool,
+ /// @internal
+ pub is_dynamic_component: Option,
+ /// @internal
+ #[serde(default)]
+ pub enable_element_template: bool,
+}
+
+impl Default for JSXTransformerConfig {
+ fn default() -> Self {
+ Self {
+ preserve_jsx: false,
+ runtime_pkg: "@lynx-js/react".into(),
+ jsx_import_source: Some("@lynx-js/react".into()),
+ filename: Default::default(),
+ target: TransformTarget::LEPUS,
+ enable_ui_source_map: false,
+ is_dynamic_component: Some(false),
+ enable_element_template: false,
+ }
+ }
+}
+
+pub struct JSXTransformer
+where
+ C: Comments + Clone,
+{
+ // react_transformer: Box,
+ cfg: JSXTransformerConfig,
+ filename_hash: String,
+ pub content_hash: String,
+ runtime_id: Lazy,
+ runtime_components_ident: Ident,
+ runtime_components_module_item: Option,
+ css_id_value: Option,
+ has_explicit_css_id: bool,
+ pub element_templates: Option>>>,
+ snapshot_counter: u32,
+ current_snapshot_defs: Vec,
+ current_snapshot_id: Option,
+ comments: Option,
+ slot_ident: Ident,
+ used_slot: bool,
+ pub ui_source_map_records: Rc>>,
+ pub source_map: Option>,
+}
+
+impl JSXTransformer
+where
+ C: Comments + Clone,
+{
+ pub fn with_content_hash(mut self, content_hash: String) -> Self {
+ self.content_hash = content_hash;
+ self
+ }
+
+ pub fn new(
+ cfg: JSXTransformerConfig,
+ comments: Option,
+ mode: TransformMode,
+ source_map: Option>,
+ ) -> Self {
+ Self::new_with_element_templates(cfg, comments, mode, source_map, None)
+ }
+
+ pub fn new_with_element_templates(
+ cfg: JSXTransformerConfig,
+ comments: Option,
+ mode: TransformMode,
+ source_map: Option>,
+ element_templates: Option>>>,
+ ) -> Self {
+ JSXTransformer {
+ filename_hash: calc_hash(&cfg.filename.clone()),
+ content_hash: "test".into(),
+ runtime_id: match mode {
+ TransformMode::Development => {
+ // We should find a way to use `cfg.runtime_pkg`
+ Lazy::new(|| quote!("require('@lynx-js/react/internal')" as Expr))
+ }
+ TransformMode::Production | TransformMode::Test => {
+ Lazy::new(|| Expr::Ident(private_ident!("ReactLynx")))
+ }
+ },
+ runtime_components_ident: private_ident!("ReactLynxRuntimeComponents"),
+ runtime_components_module_item: None,
+ element_templates,
+ cfg,
+ css_id_value: None,
+ has_explicit_css_id: false,
+ snapshot_counter: 0,
+ current_snapshot_defs: vec![],
+ current_snapshot_id: None,
+ comments,
+ slot_ident: private_ident!("__etSlot"),
+ used_slot: false,
+ ui_source_map_records: Rc::new(RefCell::new(vec![])),
+ source_map,
+ }
+ }
+
+ fn parse_directives(&mut self, span: Span) {
+ self.comments.with_leading(span.lo, |comments| {
+ for cmt in comments {
+ if cmt.kind != CommentKind::Block {
+ continue;
+ }
+ for line in cmt.text.lines() {
+ let mut line = line.trim();
+ if line.starts_with('*') {
+ line = line[1..].trim();
+ }
+
+ if !line.starts_with("@jsx") {
+ continue;
+ }
+
+ let mut words = line.split_whitespace();
+ loop {
+ let pragma = words.next();
+ if pragma.is_none() {
+ break;
+ }
+ let val = words.next();
+ if let Some("@jsxCSSId") = pragma {
+ if let Some(css_id) = val {
+ self.css_id_value = Some(Expr::Lit(Lit::Num(
+ css_id
+ .parse::()
+ .expect("should have numeric cssId")
+ .into(),
+ )));
+ self.has_explicit_css_id = true;
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+}
+
+impl VisitMut for JSXTransformer
+where
+ C: Comments + Clone,
+{
+ fn visit_mut_jsx_element(&mut self, node: &mut JSXElement) {
+ match *jsx_name(node.opening.name.clone()) {
+ Expr::Lit(lit) => {
+ if let Lit::Str(s) = &lit {
+ let tag = s.value.to_string_lossy();
+ let tag_str = tag.as_ref();
+ if tag_str == "wrapper" {
+ return node.visit_mut_children_with(self);
+ }
+ if tag_str == "page" {
+ if self.runtime_components_module_item.is_none() {
+ self.runtime_components_module_item = Some(quote!(
+ r#"import * as $runtime_components_ident from '@lynx-js/react/runtime-components';"#
+ as ModuleItem,
+ runtime_components_ident = self.runtime_components_ident.clone(),
+ ));
+ }
+
+ if let JSXElementName::Ident(_ident) = &mut node.opening.name {
+ node.opening.name = JSXElementName::JSXMemberExpr(JSXMemberExpr {
+ obj: JSXObject::Ident(self.runtime_components_ident.clone()),
+ prop: private_ident!("Page").into(),
+ span: node.opening.span,
+ });
+
+ if let Some(JSXClosingElement { name, .. }) = &mut node.closing {
+ if let JSXElementName::Ident(ident) = name {
+ *name = JSXElementName::JSXMemberExpr(JSXMemberExpr {
+ obj: JSXObject::Ident(self.runtime_components_ident.clone()),
+ prop: private_ident!("Page").into(),
+ span: ident.span(),
+ });
+ }
+ }
+ }
+ return node.visit_mut_children_with(self);
+ }
+
+ if tag_str == "component" {
+ HANDLER.with(|handler| {
+ handler
+ .struct_span_err(node.opening.name.span(), " is not supported")
+ .emit()
+ });
+ }
+ }
+ }
+ _ => {
+ return node.visit_mut_children_with(self);
+ }
+ }
+
+ self.snapshot_counter += 1;
+
+ let use_element_template = self.cfg.enable_element_template && self.element_templates.is_some();
+ let snapshot_uid_prefix = if use_element_template {
+ "_et"
+ } else {
+ "__snapshot"
+ };
+ let snapshot_uid = format!(
+ "{}_{}_{}_{}",
+ snapshot_uid_prefix, self.filename_hash, self.content_hash, self.snapshot_counter
+ );
+ let snapshot_id = Ident::new(
+ // format!("__snapshot_{}", snapshot_uid).into(),
+ snapshot_uid.clone().into(),
+ DUMMY_SP,
+ SyntaxContext::default().apply_mark(Mark::fresh(Mark::root())),
+ );
+
+ let target = self.cfg.target;
+ let runtime_id = self.runtime_id.clone();
+ let enable_element_template = use_element_template;
+ let filename_hash = self.filename_hash.clone();
+ let content_hash = self.content_hash.clone();
+ let ui_source_map_records = self.ui_source_map_records.clone();
+ let snapshot_uid_for_captured = snapshot_uid.clone();
+ let source_map = self.source_map.clone();
+ let node_index_fn = move |span: Span| {
+ let ui_source_map =
+ calc_hash_number(&format!("{}:{}:{}", filename_hash, content_hash, span.lo.0));
+
+ let mut line_number = 0;
+ let mut column_number = 0;
+ if span.lo.0 > 0 {
+ if let Some(cm) = &source_map {
+ let loc = cm.lookup_char_pos(span.lo);
+ line_number = loc.line as u32;
+ column_number = loc.col.0 as u32 + 1;
+ }
+ }
+
+ ui_source_map_records.borrow_mut().push(UISourceMapRecord {
+ ui_source_map,
+ line_number,
+ column_number,
+ snapshot_id: snapshot_uid_for_captured.clone(),
+ });
+
+ Expr::Lit(Lit::Num(Number {
+ span: DUMMY_SP,
+ value: ui_source_map as f64,
+ raw: None,
+ }))
+ };
+ let (key, snapshot_creator_func, (dynamic_part_attr, dynamic_part_children)): (
+ Option,
+ Option,
+ (Vec<_>, Vec<_>),
+ ) = {
+ let mut dynamic_part_extractor = DynamicPartExtractor::new(
+ self.runtime_id.clone(),
+ self,
+ enable_element_template,
+ self.cfg.enable_ui_source_map,
+ node_index_fn,
+ );
+
+ node.visit_mut_with(&mut dynamic_part_extractor);
+
+ (
+ dynamic_part_extractor.key,
+ dynamic_part_extractor.snapshot_creator,
+ dynamic_part_extractor.dynamic_parts.into_iter().partition(
+ |dynamic_part| match dynamic_part {
+ DynamicPart::Attr(_, _, _) | DynamicPart::Spread(_, _) => true,
+ DynamicPart::Slot(_, _) | DynamicPart::ListSlot(_, _) => false,
+ },
+ ),
+ )
+ };
+
+ let mut snapshot_children: Vec = vec![];
+ let mut snapshot_slot_values: Vec = vec![];
+ let mut snapshot_dynamic_part_def: Vec