diff --git a/src/transpiling/jsx_precompile.rs b/src/transpiling/jsx_precompile.rs new file mode 100644 index 0000000..32b0c35 --- /dev/null +++ b/src/transpiling/jsx_precompile.rs @@ -0,0 +1,1774 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_utils::prepend_stmt; +use swc_ecma_utils::quote_ident; +use swc_ecma_visit::noop_visit_mut_type; +use swc_ecma_visit::VisitMut; +use swc_ecma_visit::VisitMutWith; + +pub struct JsxPrecompile { + // Specify whether to use the jsx dev runtime or not + development: bool, + // The import path to import the jsx runtime from. Will be + // `/jsx-runtime`. + import_source: String, + + // Internal state + next_index: usize, + templates: Vec<(usize, Vec)>, + // Track if we need to import `jsx` or `jsxDEV` and which identifier + // to use if we do. + import_jsx: Option, + // Track if we need to import `jsxssr` and which identifier + // to use if we do. + import_jsx_ssr: Option, + // Track if we need to import `jsxattr` and which identifier + // to use if we do. + import_jsx_attr: Option, +} + +impl Default for JsxPrecompile { + fn default() -> Self { + Self { + next_index: 0, + templates: vec![], + development: false, + import_source: "react".to_string(), + import_jsx: None, + import_jsx_ssr: None, + import_jsx_attr: None, + } + } +} + +impl JsxPrecompile { + pub fn new(import_source: String, development: bool) -> Self { + Self { + import_source, + development, + ..JsxPrecompile::default() + } + } +} + +fn create_tpl_binding_name(index: usize) -> String { + format!("$$_tpl_{index}") +} + +/// Normalize HTML attribute name casing. Depending on which part of +/// the HTML or SVG spec you look at the casings differ. Some attributes +/// are camelCased, other's kebab cased and some lowercased. When +/// developers write JSX they commonly use camelCase only, regardless +/// of the actual attribute name. The spec doesn't care about case +/// sensitivity, but we need to account for the kebab case ones and +/// developers expect attributes in an HTML document to be lowercase. +/// Custom Elements complicate this further as we cannot make any +/// assumptions if the camelCased JSX attribute should be transformed +/// to kebab-case or not. To make matters even more complex, event +/// handlers passed to JSX usually start with `on*` like `onClick`, +/// but this makes them very hard to differentiate from custom element +/// properties when they pick something like `online=""` for example. +fn normalize_dom_attr_name(name: &str) -> String { + match name { + // React specific + "htmlFor" => "for".to_string(), + "className" => "class".to_string(), + "dangerouslySetInnerHTML" => name.to_string(), + + "panose1" => "panose-1".to_string(), + "xlinkActuate" => "xlink:actuate".to_string(), + "xlinkArcrole" => "xlink:arcrole".to_string(), + + // xlink:href was removed from SVG and isn't needed + "xlinkHref" => "href".to_string(), + "xlink:href" => "href".to_string(), + + "xlinkRole" => "xlink:role".to_string(), + "xlinkShow" => "xlink:show".to_string(), + "xlinkTitle" => "xlink:title".to_string(), + "xlinkType" => "xlink:type".to_string(), + "xmlBase" => "xml:base".to_string(), + "xmlLang" => "xml:lang".to_string(), + "xmlSpace" => "xml:space".to_string(), + + // Attributes that are kebab-cased + "acceptCharset" + | "alignmentBaseline" + | "allowReorder" + | "arabicForm" + | "baselineShift" + | "capHeight" + | "clipPath" + | "clipRule" + | "colorInterpolation" + | "colorInterpolationFilters" + | "colorProfile" + | "colorRendering" + | "contentScriptType" + | "contentStyleType" + | "dominantBaseline" + | "enableBackground" + | "fillOpacity" + | "fillRule" + | "floodColor" + | "floodOpacity" + | "fontFamily" + | "fontSize" + | "fontSizeAdjust" + | "fontStretch" + | "fontStyle" + | "fontVariant" + | "fontWeight" + | "glyphName" + | "glyphOrientationHorizontal" + | "glyphOrientationVertical" + | "horizAdvX" + | "horizOriginX" + | "httpEquiv" + | "imageRendering" + | "letterSpacing" + | "lightingColor" + | "markerEnd" + | "markerMid" + | "markerStart" + | "overlinePosition" + | "overlineThickness" + | "paintOrder" + | "pointerEvents" + | "renderingIntent" + | "repeatCount" + | "repeatDur" + | "shapeRendering" + | "stopColor" + | "stopOpacity" + | "strikethroughPosition" + | "strikethroughThickness" + | "strokeDasharray" + | "strokeDashoffset" + | "strokeLinecap" + | "strokeLinejoin" + | "strokeMiterlimit" + | "strokeOpacity" + | "strokeWidth" + | "textAnchor" + | "textDecoration" + | "underlinePosition" + | "underlineThickness" + | "unicodeBidi" + | "unicodeRange" + | "unitsPerEm" + | "vAlphabetic" + | "vectorEffect" + | "vertAdvY" + | "vertOriginX" + | "vertOriginY" + | "vHanging" + | "vMathematical" + | "wordSpacing" + | "writingMode" + | "xHeight" => name + .chars() + .map(|ch| match ch { + 'A'..='Z' => format!("-{}", ch.to_lowercase()), + _ => ch.to_string(), + }) + .collect(), + _ => { + // Devs expect attributes in the HTML document to be lowercased. + name.to_lowercase() + } + } +} + +// See: https://developer.mozilla.org/en-US/docs/Glossary/Void_element +fn is_void_element(name: &str) -> bool { + matches!( + name, + "area" + | "base" + | "br" + | "col" + | "embed" + | "hr" + | "img" + | "input" + | "link" + | "meta" + | "param" + | "source" + | "track" + | "wbr" + ) +} + +fn null_arg() -> ExprOrSpread { + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))), + } +} + +fn get_attr_name(jsx_attr: &JSXAttr) -> String { + match &jsx_attr.name { + // Case: ; +"#, + r#"import { jsxssr as _jsxssr, jsxattr as _jsxattr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
Hello!
" +]; +const $$_tpl_2 = [ + "
Hello ", + "!
" +]; +const $$_tpl_3 = [ + '" +]; +const a = _jsxssr($$_tpl_1); +const b = _jsxssr($$_tpl_2, name); +const c = _jsxssr($$_tpl_3, _jsxattr("onclick", onClick), name);"#, + ); + } + + #[test] + fn convert_self_closing_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
" +]; +const a = _jsxssr($$_tpl_1);"#, + ); + + // Void elements + test_transform( + JsxPrecompile::default(), + r#"const a =

;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
" +]; +const a = _jsxssr($$_tpl_1);"#, + ); + } + + #[test] + fn normalize_attr_name_test() { + let mappings: Vec<(String, String)> = vec![ + ("htmlFor".to_string(), "for".to_string()), + ("className".to_string(), "class".to_string()), + ("xlinkRole".to_string(), "xlink:role".to_string()), + ("acceptCharset".to_string(), "accept-charset".to_string()), + ("onFoo".to_string(), "onfoo".to_string()), + ]; + + for mapping in mappings.iter() { + test_transform( + JsxPrecompile::default(), + format!("const a = ", &mapping.0) + .as_str(), + format!( + "{}\nconst $$_tpl_1 = [\n ''\n];\nconst a = _jsxssr($$_tpl_1);", + "import { jsxssr as _jsxssr } from \"react/jsx-runtime\";", + &mapping.1 + ) + .as_str(), + ); + } + + for mapping in mappings.iter() { + test_transform( + JsxPrecompile::default(), + format!("const a = foo", &mapping.0).as_str(), + format!( + "{}\nconst a = _jsx(Foo, {{\n {}: \"foo\",\n children: \"foo\"\n}});", + "import { jsx as _jsx } from \"react/jsx-runtime\";", + &mapping.1 + ) + .as_str(), + ); + } + } + + #[test] + fn boolean_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + '' +]; +const a = _jsxssr($$_tpl_1);"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsxssr as _jsxssr, jsxattr as _jsxattr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + '" +]; +const a = _jsxssr($$_tpl_1, _jsxattr("checked", false));"#, + ); + } + + #[test] + fn dynamic_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
;"#, + r#"import { jsxssr as _jsxssr, jsxattr as _jsxattr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + '
" +]; +const a = _jsxssr($$_tpl_1, _jsxattr("bar", 2));"#, + ); + } + + #[test] + fn namespace_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = foo;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + 'foo' +]; +const a = _jsxssr($$_tpl_1);"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a = foo;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + 'foo' +]; +const a = _jsxssr($$_tpl_1);"#, + ); + } + + #[test] + fn mixed_static_dynamic_props_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
foo
;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx("div", { + foo: "1", + ...props, + bar: "2", + children: "foo" +});"#, + ); + } + + #[test] + fn dangerously_html_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
foo
;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx("div", { + dangerouslySetInnerHTML: { + __html: "foo" + }, + children: "foo" +});"#, + ); + } + + #[test] + fn key_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
foo
;"#, + r#"import { jsxssr as _jsxssr, jsxattr as _jsxattr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
foo
" +]; +const a = _jsxssr($$_tpl_1, _jsxattr("key", "foo"));"#, + ); + } + + #[test] + fn key_attr_comp_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, null, "foo");"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, null, 2);"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a = foo;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + children: "foo" +}, 2);"#, + ); + } + + #[test] + fn ref_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
foo
;"#, + r#"import { jsxssr as _jsxssr, jsxattr as _jsxattr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
foo
" +]; +const a = _jsxssr($$_tpl_1, _jsxattr("ref", "foo"));"#, + ); + } + + #[test] + fn escape_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
foo
;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + '
foo
' +]; +const a = _jsxssr($$_tpl_1);"#, + ); + } + + #[test] + fn escape_text_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
"a&>'
;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
"a&>'
" +]; +const a = _jsxssr($$_tpl_1);"#, + ); + } + + #[test] + fn namespace_name_test() { + // Note: This isn't really supported anywhere, but I guess why not + test_transform( + JsxPrecompile::default(), + r#"const a = foo;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx("a:b", { + children: "foo" +});"#, + ); + } + + #[test] + fn empty_jsx_child_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =

{}

;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "

" +]; +const a = _jsxssr($$_tpl_1);"#, + ); + } + + #[test] + fn child_expr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =

{2 + 2}

;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "

", + "

" +]; +const a = _jsxssr($$_tpl_1, 2 + 2);"#, + ); + } + + #[test] + fn empty_fragment_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = <>;"#, + r#"const a = null;"#, + ); + } + + #[test] + fn fragment_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = <>foo;"#, + r#"const a = "foo";"#, + ); + } + + #[test] + fn fragment_nested_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = <><>foo;"#, + r#"const a = "foo";"#, + ); + } + + #[test] + fn fragment_mulitple_children_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = <>foo
;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "foo
", + "" +]; +const a = _jsxssr($$_tpl_1, _jsx(Foo, null));"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a =
<><>foo;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
" +]; +const $$_tpl_2 = [ + "" +]; +const a = _jsx(Foo, { + children: [ + _jsxssr($$_tpl_1), + "foo", + _jsxssr($$_tpl_2) + ] +});"#, + ); + } + + #[test] + fn nested_elements_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
foo

bar

;"#, + r#"import { jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
foo

bar

" +]; +const a = _jsxssr($$_tpl_1);"#, + ); + } + + #[test] + fn prop_spread_without_children_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx("div", { + ...props +});"#, + ); + } + + #[test] + fn prop_spread_with_children_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
hello
;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx("div", { + ...props, + children: "hello" +});"#, + ); + } + + #[test] + fn prop_spread_with_other_attrs_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
hello
;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx("div", { + foo: "1", + ...props, + bar: "2", + children: "hello" +});"#, + ); + } + + #[test] + fn component_test() { + test_transform( + JsxPrecompile::default(), + r#"const a =
;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
", + "
" +]; +const a = _jsxssr($$_tpl_1, _jsx(Foo, null));"#, + ); + } + + #[test] + fn component_outer_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, null);"#, + ); + } + + #[test] + fn component_with_props_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + required: true, + foo: "1", + bar: 2 +});"#, + ); + } + + #[test] + fn component_with_spread_props_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + ...props, + foo: "1" +});"#, + ); + } + + #[test] + fn component_with_children_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = bar;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + children: "bar" +});"#, + ); + } + + #[test] + fn component_with_children_jsx_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = hello;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "hello" +]; +const a = _jsx(Foo, { + children: _jsxssr($$_tpl_1) +});"#, + ); + } + + #[test] + fn component_with_multiple_children_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = hellofooasdf;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "hello" +]; +const a = _jsx(Foo, { + children: [ + _jsxssr($$_tpl_1), + "foo", + _jsx(Bar, null), + "asdf" + ] +});"#, + ); + } + + #[test] + fn component_with_multiple_children_2_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = hellofoo

asdf

;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "hello" +]; +const $$_tpl_2 = [ + "

asdf

" +]; +const a = _jsx(Foo, { + children: [ + _jsxssr($$_tpl_1), + "foo", + _jsx(Bar, { + children: _jsxssr($$_tpl_2) + }) + ] +});"#, + ); + } + + #[test] + fn component_child_expr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = {2 + 2};"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + children: 2 + 2 +});"#, + ); + } + + #[test] + fn component_with_jsx_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = hello
} />;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "
hello
" +]; +const a = _jsx(Foo, { + bar: _jsxssr($$_tpl_1) +});"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a = hello} />;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + bar: _jsx(Bar, { + children: "hello" + }) +});"#, + ); + } + + #[test] + fn component_with_jsx_frag_attr_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = foo} />;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + bar: "foo" +});"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a = foobar} />;"#, + r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "foo", + "bar" +]; +const a = _jsx(Foo, { + bar: _jsxssr($$_tpl_1, _jsx(Foo, null)) +});"#, + ); + } + + #[test] + fn component_with_nested_frag_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = <>foo<>;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(Foo, { + children: [ + "foo", + _jsx(Bar, null) + ] +});"#, + ); + } + + #[test] + fn component_with_jsx_member_test() { + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(ctx.Provider, { + value: null +});"#, + ); + + test_transform( + JsxPrecompile::default(), + r#"const a = ;"#, + r#"import { jsx as _jsx } from "react/jsx-runtime"; +const a = _jsx(a.b.c.d, { + value: null +});"#, + ); + } + + #[test] + fn import_source_option_test() { + test_transform( + JsxPrecompile::new("foobar".to_string(), false), + r#"const a =
foo
;"#, + r#"import { jsxssr as _jsxssr } from "foobar/jsx-runtime"; +const $$_tpl_1 = [ + "
foo
" +]; +const a = _jsxssr($$_tpl_1);"#, + ); + } + + #[test] + fn development_option_test() { + test_transform( + JsxPrecompile::new("react".to_string(), true), + r#"const a = foo;"#, + r#"import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime"; +const a = _jsxDEV(Foo, { + children: "foo" +});"#, + ); + } + + #[track_caller] + fn test_transform( + transform: impl VisitMut, + src: &str, + expected_output: &str, + ) { + let (source_map, module) = parse(src); + let mut transform_folder = as_folder(transform); + let output = print(source_map, module.fold_with(&mut transform_folder)); + assert_eq!(output, format!("{}\n", expected_output)); + } + + fn parse(src: &str) -> (Rc, Module) { + let source_map = Rc::new(SourceMap::default()); + let source_file = source_map.new_source_file( + FileName::Url(ModuleSpecifier::parse("file:///test.ts").unwrap()), + src.to_string(), + ); + let input = StringInput::from(&*source_file); + let syntax = Syntax::Typescript(TsConfig { + tsx: true, + ..Default::default() + }); + let mut parser = Parser::new(syntax, input, None); + (source_map, parser.parse_module().unwrap()) + } + + fn print(source_map: Rc, module: Module) -> String { + let mut buf = vec![]; + { + let mut writer = + Box::new(JsWriter::new(source_map.clone(), "\n", &mut buf, None)); + writer.set_indent_str(" "); // two spaces + let mut emitter = crate::swc::codegen::Emitter { + cfg: crate::swc_codegen_config(), + comments: None, + cm: source_map, + wr: writer, + }; + module.emit_with(&mut emitter).unwrap(); + } + String::from_utf8(buf).unwrap() + } +} diff --git a/src/transpiling/mod.rs b/src/transpiling/mod.rs index 4c3d9eb..24adb97 100644 --- a/src/transpiling/mod.rs +++ b/src/transpiling/mod.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use anyhow::anyhow; use anyhow::Result; +use swc_ecma_visit::as_folder; use crate::swc::ast::Program; use crate::swc::codegen::text_writer::JsWriter; @@ -32,6 +33,7 @@ use crate::ParsedSource; use std::cell::RefCell; +mod jsx_precompile; mod transforms; #[derive(Debug, Clone, Hash)] @@ -80,6 +82,10 @@ pub struct EmitOptions { pub source_map: bool, /// Should JSX be transformed. Defaults to `true`. pub transform_jsx: bool, + /// Should JSX be precompiled into static strings that need to be concatenated + /// with dynamic content. Defaults to `false`, mutually exclusive with + /// `transform_jsx`. + pub precompile_jsx: bool, /// 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`. @@ -100,6 +106,7 @@ impl Default for EmitOptions { jsx_fragment_factory: "React.Fragment".into(), jsx_import_source: None, transform_jsx: true, + precompile_jsx: false, var_decl_imports: false, } } @@ -303,6 +310,15 @@ pub fn fold_program( ), options.transform_jsx ), + Optional::new( + as_folder(jsx_precompile::JsxPrecompile::new( + options.jsx_import_source.clone().unwrap_or_default(), + options.jsx_development, + )), + options.jsx_import_source.is_some() + && !options.transform_jsx + && options.precompile_jsx + ), Optional::new( react::react( source_map.clone(), @@ -1088,4 +1104,67 @@ for (let i = 0; i < testVariable >> 1; i++) callCount++; .trim_start_matches("//# sourceMappingURL=data:application/json;base64,"); base64::decode(input).unwrap(); } + + #[test] + fn test_precompile_jsx() { + let specifier = + ModuleSpecifier::parse("https://deno.land/x/mod.tsx").unwrap(); + let source = + r#"const a = hellofoo

asdf

;"#; + let module = parse_module(ParseParams { + specifier: specifier.as_str().to_string(), + text_info: SourceTextInfo::from_string(source.to_string()), + media_type: MediaType::Tsx, + capture_tokens: false, + maybe_syntax: None, + scope_analysis: false, + }) + .unwrap(); + let mut options = EmitOptions { + transform_jsx: false, + precompile_jsx: true, + jsx_import_source: Some("react".to_string()), + ..Default::default() + }; + let code = module.transpile(&options).unwrap().text; + let expected1 = r#"import { jsx as _jsx, jsxssr as _jsxssr } from "react/jsx-runtime"; +const $$_tpl_1 = [ + "hello" +]; +const $$_tpl_2 = [ + "

asdf

" +]; +const a = _jsx(Foo, { + children: [ + _jsxssr($$_tpl_1), + "foo", + _jsx(Bar, { + children: _jsxssr($$_tpl_2) + }) + ] +}); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJza"#; + assert_eq!(&code[0..expected1.len()], expected1); + + options.jsx_development = true; + let code = module.transpile(&options).unwrap().text; + let expected2 = r#"import { jsxDEV as _jsxDEV, jsxssr as _jsxssr } from "react/jsx-dev-runtime"; +const $$_tpl_1 = [ + "hello" +]; +const $$_tpl_2 = [ + "

asdf

" +]; +const a = _jsxDEV(Foo, { + children: [ + _jsxssr($$_tpl_1), + "foo", + _jsxDEV(Bar, { + children: _jsxssr($$_tpl_2) + }) + ] +}); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJza"#; + assert_eq!(&code[0..expected2.len()], expected2); + } }