Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: option to force dynamic jsx precompile props #259

Merged
merged 1 commit into from
Jun 24, 2024
Merged
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
67 changes: 62 additions & 5 deletions src/transpiling/jsx_precompile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pub struct JsxPrecompile {
import_source: String,
// List of HTML elements which should not be serialized
skip_serialize: Option<Vec<String>>,
// List of props/attributes that should not be serialized and
// always be treated as dynamic instead.
skip_prop_serialize: Option<Vec<String>>,

// Internal state
next_index: usize,
Expand All @@ -41,6 +44,7 @@ impl Default for JsxPrecompile {
templates: vec![],
import_source: "react".to_string(),
skip_serialize: None,
skip_prop_serialize: None,
import_jsx: None,
import_jsx_ssr: None,
import_jsx_attr: None,
Expand All @@ -53,10 +57,12 @@ impl JsxPrecompile {
pub fn new(
import_source: String,
skip_serialize: Option<Vec<String>>,
skip_prop_serialize: Option<Vec<String>>,
) -> Self {
Self {
import_source,
skip_serialize,
skip_prop_serialize,
..JsxPrecompile::default()
}
}
Expand Down Expand Up @@ -250,11 +256,11 @@ fn null_arg() -> ExprOrSpread {
}
}

fn get_attr_name(jsx_attr: &JSXAttr, is_component: bool) -> String {
fn get_attr_name(jsx_attr: &JSXAttr, normalize: bool) -> String {
match &jsx_attr.name {
// Case: <button class="btn">
JSXAttrName::Ident(ident) => {
if is_component {
if !normalize {
ident.sym.to_string()
} else {
normalize_dom_attr_name(ident.sym.as_ref())
Expand Down Expand Up @@ -821,7 +827,7 @@ impl JsxPrecompile {
for attr in el.opening.attrs.iter() {
match attr {
JSXAttrOrSpread::JSXAttr(jsx_attr) => {
let attr_name = get_attr_name(jsx_attr, is_component);
let attr_name = get_attr_name(jsx_attr, !is_component);
let prop_name = if !is_text_valid_identifier(&attr_name) {
PropName::Str(Str {
span: DUMMY_SP,
Expand Down Expand Up @@ -1066,7 +1072,38 @@ impl JsxPrecompile {
// Case: <button class="btn">
match attr {
JSXAttrOrSpread::JSXAttr(jsx_attr) => {
let attr_name = get_attr_name(jsx_attr, false);
// User's can force certain attributes to always be treated
// as dynamic.
if let Some(skip_prop_serialize) = &self.skip_prop_serialize {
let attr_name = get_attr_name(jsx_attr, false);
if skip_prop_serialize.contains(&attr_name) {
strings.last_mut().unwrap().push(' ');
strings.push("".to_string());

let value = match &jsx_attr.value {
Some(attr_value) => match attr_value.clone() {
JSXAttrValue::Lit(lit) => Expr::Lit(lit),
JSXAttrValue::JSXExprContainer(_) => todo!(),
JSXAttrValue::JSXElement(jsx_element) => {
Expr::JSXElement(jsx_element)
}
JSXAttrValue::JSXFragment(jsx_frag) => {
Expr::JSXFragment(jsx_frag)
}
},
None => Expr::Lit(Lit::Bool(Bool {
span: DUMMY_SP,
value: true,
})),
};

let expr = self.convert_to_jsx_attr_call(attr_name.into(), value);
dynamic_exprs.push(Expr::Call(expr));
continue;
}
}

let attr_name = get_attr_name(jsx_attr, true);

// Case: <input required />
let Some(attr_value) = &jsx_attr.value else {
Expand Down Expand Up @@ -2489,7 +2526,7 @@ const a = _jsx(a.b.c.d, {
#[test]
fn import_source_option_test() {
test_transform(
JsxPrecompile::new("foobar".to_string(), None),
JsxPrecompile::new("foobar".to_string(), None, None),
r#"const a = <div>foo</div>;"#,
r#"import { jsxTemplate as _jsxTemplate } from "foobar/jsx-runtime";
const $$_tpl_1 = [
Expand Down Expand Up @@ -2639,6 +2676,7 @@ const a = _jsxTemplate($$_tpl_1);"#,
JsxPrecompile::new(
"react".to_string(),
Some(vec!["a".to_string(), "img".to_string()]),
None,
),
r#"const a = <div><img src="foo.jpg"/><a href="\#">foo</a></div>"#,
r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
Expand All @@ -2656,6 +2694,25 @@ const a = _jsxTemplate($$_tpl_1, _jsx("img", {
);
}

#[test]
fn skip_prop_serialization_test() {
test_transform(
JsxPrecompile::new(
"react".to_string(),
None,
Some(vec!["class".to_string(), "className".to_string()]),
),
r#"const a = <div class="foo"><img id="foo" className="foo" /></div>"#,
r#"import { jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_1 = [
"<div ",
'><img id="foo" ',
"></div>"
];
const a = _jsxTemplate($$_tpl_1, _jsxAttr("class", "foo"), _jsxAttr("className", "foo"));"#,
);
}

#[track_caller]
fn test_transform(
transform: impl VisitMut,
Expand Down
17 changes: 13 additions & 4 deletions src/transpiling/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ pub struct TranspileOptions {
/// List of elements that should not be precompiled when the JSX precompile
/// transform is used.
pub precompile_jsx_skip_elements: Option<Vec<String>>,
/// List of properties/attributes that should always be treated as
/// dynamic.
pub precompile_jsx_dynamic_props: Option<Vec<String>>,
/// Should import declarations be transformed to variable declarations using
/// a dynamic import. This is useful for import & export declaration support
/// in script contexts such as the Deno REPL. Defaults to `false`.
Expand All @@ -149,6 +152,7 @@ impl Default for TranspileOptions {
transform_jsx: true,
precompile_jsx: false,
precompile_jsx_skip_elements: None,
precompile_jsx_dynamic_props: None,
var_decl_imports: false,
}
}
Expand Down Expand Up @@ -426,6 +430,7 @@ pub fn fold_program(
as_folder(jsx_precompile::JsxPrecompile::new(
options.jsx_import_source.clone().unwrap_or_default(),
options.precompile_jsx_skip_elements.clone(),
options.precompile_jsx_dynamic_props.clone(),
)),
options.jsx_import_source.is_some()
&& !options.transform_jsx
Expand Down Expand Up @@ -1488,8 +1493,7 @@ for (let i = 0; i < testVariable >> 1; i++) callCount++;
fn test_precompile_jsx() {
let specifier =
ModuleSpecifier::parse("https://deno.land/x/mod.tsx").unwrap();
let source =
r#"const a = <Foo><span>hello</span>foo<Bar><p>asdf</p></Bar></Foo>;"#;
let source = r#"const a = <Foo><span>hello</span>foo<Bar><p><span class="bar">asdf</span></p></Bar></Foo>;"#;
let module = parse_module(ParseParams {
specifier,
text: source.into(),
Expand All @@ -1503,6 +1507,7 @@ for (let i = 0; i < testVariable >> 1; i++) callCount++;
transform_jsx: false,
precompile_jsx: true,
precompile_jsx_skip_elements: Some(vec!["p".to_string()]),
precompile_jsx_dynamic_props: Some(vec!["class".to_string()]),
jsx_import_source: Some("react".to_string()),
..Default::default()
};
Expand All @@ -1513,15 +1518,19 @@ for (let i = 0; i < testVariable >> 1; i++) callCount++;
.into_string()
.unwrap()
.text;
let expected1 = r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate } from "react/jsx-runtime";
let expected1 = r#"import { jsx as _jsx, jsxTemplate as _jsxTemplate, jsxAttr as _jsxAttr } from "react/jsx-runtime";
const $$_tpl_2 = [
"<span ",
">asdf</span>"
];
const $$_tpl_1 = [
"<span>hello</span>foo",
""
];
const a = _jsx(Foo, {
children: _jsxTemplate($$_tpl_1, _jsx(Bar, {
children: _jsx("p", {
children: "asdf"
children: _jsxTemplate($$_tpl_2, _jsxAttr("class", "bar"))
})
}))
});
Expand Down