Skip to content

Commit 64a29eb

Browse files
nojafcristianoc
andauthored
Preserve JSX (#7387)
* Add additional node to Pexp_apply * Try and pass jsx_element from typed_tree to js_call * Follow Lprim * Transform initial simple element * Initial fragment support * WIP extract, good stuff * Print props, catch with key functions * Don't pass untyped ast, simple flag is sufficient. * Support fragments * Unwrap children * Poor man feature flag * Remove duplicated in * Older camls * Revert "Poor man feature flag" This reverts commit 547dfca. * Add new -bs-jsx-preserve flag * WIP, deal with prop spreading * Deal with prop spreading * Clean up Lprim and move the information to the call primitive. * Add test for spreading children * Refactor duplicate code * Support keyed and prop spreading * Rename test file * Keep key prop before spreading props. Also ensure test can run. * Add only spread props case * Give record a name * Use config flag in Js_dump instead * Remove helper code * Extra call info * Detect direct Array as well * Don't run with mocha * Feedback code review --------- Co-authored-by: Cristiano Calcagno <[email protected]>
1 parent 5accae3 commit 64a29eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+787
-131
lines changed

analysis/src/CompletionFrontEnd.ml

+12-10
Original file line numberDiff line numberDiff line change
@@ -267,33 +267,35 @@ let rec exprToContextPathInner ~(inJsxContext : bool) (e : Parsetree.expression)
267267
};
268268
args =
269269
[(_, lhs); (_, {pexp_desc = Pexp_apply {funct = d; args; partial}})];
270+
transformed_jsx;
270271
} ->
271272
(* Transform away pipe with apply call *)
272273
exprToContextPath ~inJsxContext
273274
{
274275
pexp_desc =
275-
Pexp_apply {funct = d; args = (Nolabel, lhs) :: args; partial};
276+
Pexp_apply
277+
{funct = d; args = (Nolabel, lhs) :: args; partial; transformed_jsx};
276278
pexp_loc;
277279
pexp_attributes;
278280
}
279281
| Pexp_apply
280-
{
281-
funct = {pexp_desc = Pexp_ident {txt = Lident "->"}};
282-
args =
283-
[
284-
(_, lhs); (_, {pexp_desc = Pexp_ident id; pexp_loc; pexp_attributes});
285-
];
286-
partial;
287-
} ->
282+
({
283+
funct = {pexp_desc = Pexp_ident {txt = Lident "->"}};
284+
args =
285+
[
286+
(_, lhs);
287+
(_, {pexp_desc = Pexp_ident id; pexp_loc; pexp_attributes});
288+
];
289+
} as app) ->
288290
(* Transform away pipe with identifier *)
289291
exprToContextPath ~inJsxContext
290292
{
291293
pexp_desc =
292294
Pexp_apply
293295
{
296+
app with
294297
funct = {pexp_desc = Pexp_ident id; pexp_loc; pexp_attributes};
295298
args = [(Nolabel, lhs)];
296-
partial;
297299
};
298300
pexp_loc;
299301
pexp_attributes;

compiler/bsc/rescript_compiler_main.ml

+1
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ let buckle_script_flags : (string * Bsc_args.spec * string) array =
255255
( "-bs-jsx-mode",
256256
string_call ignore,
257257
"*internal* Set jsx mode, this is no longer used and is a no-op." );
258+
("-bs-jsx-preserve", set Js_config.jsx_preserve, "*internal* Preserve jsx");
258259
( "-bs-package-output",
259260
string_call Js_packages_state.update_npm_package_path,
260261
"*internal* Set npm-output-path: [opt_module]:path, for example: \

compiler/common/js_config.ml

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ let force_cmi = ref false
5050
let force_cmj = ref false
5151
let jsx_version = ref None
5252
let jsx_module = ref React
53+
let jsx_preserve = ref false
5354
let js_stdout = ref true
5455
let all_module_aliases = ref false
5556
let no_stdlib = ref false

compiler/common/js_config.mli

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ val jsx_version : jsx_version option ref
8080

8181
val jsx_module : jsx_module ref
8282

83+
val jsx_preserve : bool ref
84+
8385
val js_stdout : bool ref
8486

8587
val all_module_aliases : bool ref

compiler/core/js_call_info.ml

+9-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,15 @@ type call_info =
3333
{[ fun x y -> (f x y) === f ]} when [f] is an atom
3434
*)
3535

36-
type t = {call_info: call_info; arity: arity}
36+
type t = {call_info: call_info; arity: arity; call_transformed_jsx: bool}
3737

38-
let dummy = {arity = NA; call_info = Call_na}
38+
let dummy = {arity = NA; call_info = Call_na; call_transformed_jsx = false}
3939

40-
let builtin_runtime_call = {arity = Full; call_info = Call_builtin_runtime}
40+
let builtin_runtime_call =
41+
{arity = Full; call_info = Call_builtin_runtime; call_transformed_jsx = false}
4142

42-
let ml_full_call = {arity = Full; call_info = Call_ml}
43+
let ml_full_call =
44+
{arity = Full; call_info = Call_ml; call_transformed_jsx = false}
45+
46+
let na_full_call transformed_jsx =
47+
{arity = Full; call_info = Call_na; call_transformed_jsx = transformed_jsx}

compiler/core/js_call_info.mli

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ type call_info =
3535
{[ fun x y -> f x y === f ]} when [f] is an atom
3636
*)
3737

38-
type t = {call_info: call_info; arity: arity}
38+
type t = {call_info: call_info; arity: arity; call_transformed_jsx: bool}
3939

4040
val dummy : t
4141

4242
val builtin_runtime_call : t
4343

4444
val ml_full_call : t
45+
46+
val na_full_call : bool -> t

compiler/core/js_dump.ml

+208-1
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ and expression_desc cxt ~(level : int) f x : cxt =
515515
(* TODO: dump for comments *)
516516
pp_function ?directive ~is_method ~return_unit ~async
517517
~fn_state:default_fn_exp_state cxt f params body env
518-
(* TODO:
518+
(* TODO:
519519
when [e] is [Js_raw_code] with arity
520520
print it in a more precise way
521521
It seems the optimizer already did work to make sure
@@ -524,6 +524,116 @@ and expression_desc cxt ~(level : int) f x : cxt =
524524
when Ext_list.length_equal el i
525525
]}
526526
*)
527+
(* When -bs-preserve-jsx is enabled, we marked each transformed application node throughout the compilation.
528+
Here we print the transformed application node into a JSX syntax.
529+
The JSX is slightly different from what a user would write,
530+
but it is still valid JSX and is usable by tools like ESBuild.
531+
*)
532+
| Call
533+
( ({
534+
expression_desc =
535+
J.Var
536+
(J.Qualified
537+
( _,
538+
Some fnName
539+
(* We care about the function name when it is jsxs,
540+
If this is the case, we need to unpack an array later on *)
541+
));
542+
} as e),
543+
el,
544+
{call_transformed_jsx = true} )
545+
when !Js_config.jsx_preserve -> (
546+
(* We match a JsxRuntime.jsx call *)
547+
match el with
548+
| [
549+
tag;
550+
{
551+
expression_desc =
552+
(* This is the props javascript object *)
553+
Caml_block (el, _mutable_flag, _, Lambda.Blk_record {fields});
554+
};
555+
] ->
556+
(* We extract the props from the javascript object *)
557+
let fields =
558+
Ext_list.array_list_filter_map fields el (fun (f, opt) x ->
559+
match x.expression_desc with
560+
| Undefined _ when opt -> None
561+
| _ -> Some (f, x))
562+
in
563+
print_jsx cxt ~level f fnName tag fields
564+
| [
565+
tag;
566+
{
567+
expression_desc =
568+
Caml_block (el, _mutable_flag, _, Lambda.Blk_record {fields});
569+
};
570+
key;
571+
] ->
572+
(* When a component has a key the matching runtime function call will have a third argument being the key *)
573+
let fields =
574+
Ext_list.array_list_filter_map fields el (fun (f, opt) x ->
575+
match x.expression_desc with
576+
| Undefined _ when opt -> None
577+
| _ -> Some (f, x))
578+
in
579+
print_jsx cxt ~level ~key f fnName tag fields
580+
| [tag; ({expression_desc = J.Seq _} as props)] ->
581+
(* In the case of prop spreading, the expression will look like:
582+
(props.a = "Hello, world!", props)
583+
which is equivalent to
584+
<tag {...props} a="Hello, world!" />
585+
586+
We need to extract the props and the spread object.
587+
*)
588+
let fields, spread_props =
589+
let rec visit acc e =
590+
match e.J.expression_desc with
591+
| J.Seq
592+
( {
593+
J.expression_desc =
594+
J.Bin
595+
( Js_op.Eq,
596+
{J.expression_desc = J.Static_index (_, name, _)},
597+
value );
598+
},
599+
rest ) ->
600+
visit ((name, value) :: acc) rest
601+
| _ -> (List.rev acc, e)
602+
in
603+
visit [] props
604+
in
605+
print_jsx cxt ~level ~spread_props f fnName tag fields
606+
| [tag; ({expression_desc = J.Seq _} as props); key] ->
607+
(* In the case of props + prop spreading and key argument *)
608+
let fields, spread_props =
609+
let rec visit acc e =
610+
match e.J.expression_desc with
611+
| J.Seq
612+
( {
613+
J.expression_desc =
614+
J.Bin
615+
( Js_op.Eq,
616+
{J.expression_desc = J.Static_index (_, name, _)},
617+
value );
618+
},
619+
rest ) ->
620+
visit ((name, value) :: acc) rest
621+
| _ -> (List.rev acc, e)
622+
in
623+
visit [] props
624+
in
625+
print_jsx cxt ~level ~spread_props ~key f fnName tag fields
626+
| [tag; ({expression_desc = J.Var _} as spread_props)] ->
627+
(* All the props are spread *)
628+
print_jsx cxt ~level ~spread_props f fnName tag []
629+
| _ ->
630+
(* This should not happen, we fallback to the general case *)
631+
expression_desc cxt ~level f
632+
(Call
633+
( e,
634+
el,
635+
{call_transformed_jsx = false; arity = Full; call_info = Call_ml}
636+
)))
527637
| Call (e, el, info) ->
528638
P.cond_paren_group f (level > 15) (fun _ ->
529639
P.group f 0 (fun _ ->
@@ -960,6 +1070,103 @@ and expression_desc cxt ~(level : int) f x : cxt =
9601070
P.string f "...";
9611071
expression ~level:13 cxt f e)
9621072

1073+
and print_jsx cxt ?(spread_props : J.expression option)
1074+
?(key : J.expression option) ~(level : int) f (fnName : string)
1075+
(tag : J.expression) (fields : (string * J.expression) list) : cxt =
1076+
let print_tag cxt =
1077+
match tag.expression_desc with
1078+
(* "div" or any other primitive tag *)
1079+
| J.Str {txt} ->
1080+
P.string f txt;
1081+
cxt
1082+
(* fragment *)
1083+
| J.Var (J.Qualified ({id = {name = "JsxRuntime"}}, Some "Fragment")) -> cxt
1084+
(* A user defined component or external component *)
1085+
| _ -> expression ~level cxt f tag
1086+
in
1087+
let children_opt =
1088+
List.find_map
1089+
(fun (n, e) ->
1090+
if n = "children" then
1091+
if fnName = "jsxs" then
1092+
match e.J.expression_desc with
1093+
| J.Array (xs, _)
1094+
| J.Optional_block ({expression_desc = J.Array (xs, _)}, _) ->
1095+
Some xs
1096+
| _ -> Some [e]
1097+
else Some [e]
1098+
else None)
1099+
fields
1100+
in
1101+
let print_props cxt =
1102+
(* If a key is present, should be printed before the spread props,
1103+
This is to ensure tools like ESBuild use the automatic JSX runtime *)
1104+
let cxt =
1105+
match key with
1106+
| None -> cxt
1107+
| Some key ->
1108+
P.string f " key={";
1109+
let cxt = expression ~level:0 cxt f key in
1110+
P.string f "} ";
1111+
cxt
1112+
in
1113+
let props = List.filter (fun (n, _) -> n <> "children") fields in
1114+
let cxt =
1115+
match spread_props with
1116+
| None -> cxt
1117+
| Some spread ->
1118+
P.string f " {...";
1119+
let cxt = expression ~level:0 cxt f spread in
1120+
P.string f "} ";
1121+
cxt
1122+
in
1123+
if List.length props = 0 then cxt
1124+
else
1125+
(List.fold_left (fun acc (n, x) ->
1126+
P.space f;
1127+
P.string f n;
1128+
P.string f "=";
1129+
P.string f "{";
1130+
let next = expression ~level:0 acc f x in
1131+
P.string f "}";
1132+
next))
1133+
cxt props
1134+
in
1135+
match children_opt with
1136+
| None ->
1137+
P.string f "<";
1138+
let cxt = cxt |> print_tag |> print_props in
1139+
P.string f "/>";
1140+
cxt
1141+
| Some children ->
1142+
let child_is_jsx child =
1143+
match child.J.expression_desc with
1144+
| J.Call (_, _, {call_transformed_jsx = is_jsx}) -> is_jsx
1145+
| _ -> false
1146+
in
1147+
1148+
P.string f "<";
1149+
let cxt = cxt |> print_tag |> print_props in
1150+
1151+
P.string f ">";
1152+
if List.length children > 0 then P.newline f;
1153+
1154+
let cxt =
1155+
List.fold_left
1156+
(fun acc e ->
1157+
if not (child_is_jsx e) then P.string f "{";
1158+
let next = expression ~level acc f e in
1159+
if not (child_is_jsx e) then P.string f "}";
1160+
P.newline f;
1161+
next)
1162+
cxt children
1163+
in
1164+
1165+
P.string f "</";
1166+
let cxt = print_tag cxt in
1167+
P.string f ">";
1168+
cxt
1169+
9631170
and property_name_and_value_list cxt f (l : J.property_map) =
9641171
iter_lst cxt f l
9651172
(fun cxt f (pn, e) ->

0 commit comments

Comments
 (0)