Source file server_reason_react_ppx.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
open Ppxlib
open Ast_builder.Default
module List = ListLabels
type target = Native | Js
let mode = ref Native
let shared_folder_prefix = ref None
let repo_url = "https://github.com/ml-in-barcelona/server-reason-react"
let issues_url = Printf.sprintf "%s/issues" repo_url
let match_substring string substring =
try
Str.search_forward (Str.regexp_string substring) string 0 |> ignore;
true
with Not_found -> false
let pexp_list ~loc xs =
List.fold_left (List.rev xs) ~init:[%expr []] ~f:(fun xs x ->
let loc = x.pexp_loc in
[%expr [%e x] :: [%e xs]])
exception Error of expression
let raise_errorf ~loc fmt =
Printf.ksprintf
(fun msg ->
let expr = pexp_extension ~loc (Location.error_extensionf ~loc "%s" msg) in
raise (Error expr))
fmt
let longident ~loc txt = { txt = Lident txt; loc }
let ident ~loc txt = pexp_ident ~loc (longident ~loc txt)
let make_string ~loc str = Ast_helper.Exp.constant ~loc (Ast_helper.Const.string str)
let react_dot_component = "react.component"
let react_dot_async_dot_component = "react.async.component"
let react_dot_client_dot_component = "react.client.component"
let react_dot_server_dot_function = "react.server.function"
let hasAttr { attr_name; _ } comparable = attr_name.txt = comparable
let hasAnyReactComponentAttribute { attr_name; _ } =
attr_name.txt = react_dot_component
|| attr_name.txt = react_dot_async_dot_component
|| attr_name.txt = react_dot_client_dot_component
let nonReactAttributes { attr_name; _ } =
attr_name.txt <> react_dot_component
&& attr_name.txt <> react_dot_async_dot_component
&& attr_name.txt <> react_dot_client_dot_component
let hasAttrOnBinding { pvb_attributes } comparable =
List.find_opt ~f:(fun attr -> hasAttr attr comparable) pvb_attributes <> None
let isReactComponentBinding vb = hasAttrOnBinding vb react_dot_component
let isReactAsyncComponentBinding vb = hasAttrOnBinding vb react_dot_async_dot_component
let isReactClientComponentBinding vb = hasAttrOnBinding vb react_dot_client_dot_component
let isReactServerFunctionBinding vb = hasAttrOnBinding vb react_dot_server_dot_function
let isClientComponentBinding value_bindings =
let first_binding = List.hd value_bindings in
isReactClientComponentBinding first_binding
let contains_client_component structure =
List.exists
~f:(fun structure_item ->
match structure_item.pstr_desc with
| Pstr_value (_, value_bindings) -> List.exists ~f:isReactClientComponentBinding value_bindings
| _ -> false)
structure
let rec unwrap_children children = function
| { pexp_desc = Pexp_construct ({ txt = Lident "[]"; _ }, None); _ } -> List.rev children
| { pexp_desc = Pexp_construct ({ txt = Lident "::"; _ }, Some { pexp_desc = Pexp_tuple [ child; next ]; _ }); _ } ->
unwrap_children (child :: children) next
| e -> raise_errorf ~loc:e.pexp_loc "jsx: children prop should be a list"
let is_jsx = function { attr_name = { txt = "JSX"; _ }; _ } -> true | _ -> false
let has_jsx_attr attrs = List.exists ~f:is_jsx attrs
let rewrite_component ~loc tag args children =
let component = pexp_ident ~loc tag in
let props =
match children with
| None -> args
| Some [ children ] -> (Labelled "children", children) :: args
| Some children -> (Labelled "children", [%expr React.list [%e pexp_list ~loc children]]) :: args
in
pexp_apply ~loc component props
let validate_prop ~loc id name =
match DomProps.findByJsxName ~tag:id name with
| Ok p -> p
| Error `ElementNotFound ->
raise_errorf ~loc "jsx: HTML tag '%s' doesn't exist.\nIf this isn't correct, please open an issue at %s" id
issues_url
| Error `AttributeNotFound -> (
match DomProps.findClosestName name with
| None ->
raise_errorf ~loc
"jsx: prop '%s' isn't valid on a '%s' element.\nIf this isn't correct, please open an issue at %s." name id
issues_url
| Some suggestion ->
raise_errorf ~loc
"jsx: prop '%s' isn't valid on a '%s' element.\n\
Hint: Maybe you mean '%s'?\n\n\
If this isn't correct, please open an issue at %s."
name id suggestion issues_url)
let make_prop ~is_optional ~prop attribute_value =
let loc = attribute_value.pexp_loc in
let open DomProps in
match (prop, is_optional) with
| Attribute { type_ = DomProps.Action; name; jsxName }, false ->
[%expr
match ([%e attribute_value] : [ `String of string | `Function of 'a Runtime.server_function ]) with
| `String s -> Some (React.JSX.String ([%e estring ~loc name], [%e estring ~loc jsxName], (s : string)))
| `Function f ->
Some
(React.JSX.Action ([%e estring ~loc name], [%e estring ~loc jsxName], (f : 'a Runtime.server_function)))]
| Attribute { type_ = DomProps.Action; name; jsxName }, true ->
[%expr
match ([%e attribute_value] : [ `String of string | `Function of 'a Runtime.server_function ] option) with
| None -> None
| Some v -> Some (React.JSX.Action ([%e estring ~loc name], [%e estring ~loc jsxName], v))]
| Attribute { type_ = DomProps.String; name; jsxName }, false ->
[%expr
Some (React.JSX.String ([%e estring ~loc name], [%e estring ~loc jsxName], ([%e attribute_value] : string)))]
| Attribute { type_ = DomProps.String; name; jsxName }, true ->
[%expr
match ([%e attribute_value] : string option) with
| None -> None
| Some v -> Some (React.JSX.String ([%e estring ~loc name], [%e estring ~loc jsxName], v))]
| Attribute { type_ = DomProps.Int; name; jsxName }, false ->
[%expr
Some
(React.JSX.String
([%e estring ~loc name], [%e estring ~loc jsxName], Stdlib.Int.to_string ([%e attribute_value] : int)))]
| Attribute { type_ = DomProps.Int; name; jsxName }, true ->
[%expr
match ([%e attribute_value] : int option) with
| None -> None
| Some v -> Some (React.JSX.String ([%e estring ~loc name], [%e estring ~loc jsxName], Stdlib.Int.to_string v))]
| Attribute { type_ = DomProps.Bool; name; jsxName }, false ->
[%expr Some (React.JSX.Bool ([%e estring ~loc name], [%e estring ~loc jsxName], ([%e attribute_value] : bool)))]
| Attribute { type_ = DomProps.Bool; name; jsxName }, true ->
[%expr
match ([%e attribute_value] : bool option) with
| None -> None
| Some v -> Some (React.JSX.Bool ([%e estring ~loc name], [%e estring ~loc jsxName], v))]
| Attribute { type_ = DomProps.BooleanishString; name; jsxName }, false ->
[%expr
Some
(React.JSX.String
([%e estring ~loc name], [%e estring ~loc jsxName], Stdlib.Bool.to_string ([%e attribute_value] : bool)))]
| Attribute { type_ = DomProps.BooleanishString; name; jsxName }, true ->
[%expr
match ([%e attribute_value] : bool option) with
| None -> None
| Some v -> Some (React.JSX.String ([%e estring ~loc name], [%e estring ~loc jsxName], Stdlib.Bool.to_string v))]
| Attribute { type_ = DomProps.Style; _ }, false ->
[%expr Some (React.JSX.Style ([%e attribute_value] : ReactDOM.Style.t))]
| Attribute { type_ = DomProps.Style; _ }, true ->
[%expr
match ([%e attribute_value] : ReactDOM.Style.t option) with None -> None | Some v -> Some (React.JSX.Style v)]
| Attribute { type_ = DomProps.Ref; _ }, false -> [%expr Some (React.JSX.Ref ([%e attribute_value] : React.domRef))]
| Attribute { type_ = DomProps.Ref; _ }, true ->
[%expr match ([%e attribute_value] : React.domRef option) with None -> None | Some v -> Some (React.JSX.Ref v)]
| Attribute { type_ = DomProps.InnerHtml; _ }, false ->
[%expr Some (React.JSX.dangerouslyInnerHtml [%e attribute_value])]
| Attribute { type_ = DomProps.InnerHtml; _ }, true ->
[%expr match [%e attribute_value] with None -> None | Some v -> Some (React.JSX.dangerouslyInnerHtml v)]
| Event { type_ = Mouse; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Mouse ([%e attribute_value] : React.Event.Mouse.t -> unit)))]
| Event { type_ = Mouse; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Mouse.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Mouse v))]
| Event { type_ = Selection; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Selection ([%e attribute_value] : React.Event.Mouse.t -> unit)))]
| Event { type_ = Selection; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Selection.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Selection v))]
| Event { type_ = Touch; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Touch ([%e attribute_value] : React.Event.Touch.t -> unit)))]
| Event { type_ = Touch; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Touch.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Touch v))]
| Event { type_ = UI; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.UI ([%e attribute_value] : React.Event.UI.t -> unit)))]
| Event { type_ = UI; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.UI.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.UI v))]
| Event { type_ = Wheel; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Wheel ([%e attribute_value] : React.Event.Wheel.t -> unit)))]
| Event { type_ = Wheel; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Wheel.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Wheel v))]
| Event { type_ = Clipboard; jsxName }, false ->
[%expr
Some
(React.JSX.Event
( [%e make_string ~loc jsxName],
React.JSX.Clipboard ([%e attribute_value] : React.Event.Clipboard.t -> unit) ))]
| Event { type_ = Clipboard; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Clipboard.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Clipboard v))]
| Event { type_ = Composition; jsxName }, false ->
[%expr
Some
(React.JSX.Event
( [%e make_string ~loc jsxName],
React.JSX.Composition ([%e attribute_value] : React.Event.Composition.t -> unit) ))]
| Event { type_ = Composition; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Composition.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Composition v))]
| Event { type_ = Keyboard; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Keyboard ([%e attribute_value] : React.Event.Keyboard.t -> unit)))]
| Event { type_ = Keyboard; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Keyboard.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Keyboard v))]
| Event { type_ = Focus; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Focus ([%e attribute_value] : React.Event.Focus.t -> unit)))]
| Event { type_ = Focus; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Focus.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Focus v))]
| Event { type_ = Form; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Form ([%e attribute_value] : React.Event.Form.t -> unit)))]
| Event { type_ = Form; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Form.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Form v))]
| Event { type_ = Media; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Media ([%e attribute_value] : React.Event.Media.t -> unit)))]
| Event { type_ = Media; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Media.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Media v))]
| Event { type_ = Inline; jsxName }, false ->
[%expr Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Inline ([%e attribute_value] : string)))]
| Event { type_ = Inline; jsxName }, true ->
[%expr
match ([%e attribute_value] : string option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Inline v))]
| Event { type_ = Image; jsxName }, false ->
[%expr
Some
(React.JSX.Event
( [%e make_string ~loc jsxName],
React.JSX.Image ([%e attribute_value] : (React.Event.Image.t -> unit) option) ))]
| Event { type_ = Image; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Image.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Image v))]
| Event { type_ = Animation; jsxName }, false ->
[%expr
Some
(React.JSX.Event
( [%e make_string ~loc jsxName],
React.JSX.Animation ([%e attribute_value] : React.Event.Animation.t -> unit) ))]
| Event { type_ = Animation; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Animation.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Animation v))]
| Event { type_ = Transition; jsxName }, false ->
[%expr
Some
(React.JSX.Event
( [%e make_string ~loc jsxName],
React.JSX.Transition ([%e attribute_value] : React.Event.Transition.t -> unit) ))]
| Event { type_ = Transition; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Transition.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Transition v))]
| Event { type_ = Pointer; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Pointer ([%e attribute_value] : React.Event.Pointer.t -> unit)))]
| Event { type_ = Pointer; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Pointer.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Pointer v))]
| Event { type_ = Drag; jsxName }, false ->
[%expr
Some
(React.JSX.Event
([%e make_string ~loc jsxName], React.JSX.Drag ([%e attribute_value] : React.Event.Drag.t -> unit)))]
| Event { type_ = Drag; jsxName }, true ->
[%expr
match ([%e attribute_value] : (React.Event.Drag.t -> unit) option) with
| None -> None
| Some v -> Some (React.JSX.Event ([%e make_string ~loc jsxName], React.JSX.Drag v))]
let is_optional = function Optional _ -> true | _ -> false
let get_label = function Nolabel -> "" | Optional name | Labelled name -> name
let transform_labelled ~loc ~tag_name (prop_label, (runtime_value : expression)) props =
match prop_label with
| Nolabel -> props
| Optional name | Labelled name ->
let is_optional = is_optional prop_label in
let prop = validate_prop ~loc tag_name name in
let new_prop = make_prop ~is_optional ~prop runtime_value in
[%expr [%e new_prop] :: [%e props]]
let transform_lowercase_props ~loc ~tag_name args =
match args with
| [] -> [%expr []]
| attrs -> (
let list_of_attributes = attrs |> List.fold_right ~f:(transform_labelled ~loc ~tag_name) ~init:[%expr []] in
match list_of_attributes with
| [%expr []] -> [%expr []]
| _ ->
[%expr Stdlib.List.filter_map Stdlib.Fun.id [%e list_of_attributes]])
let rewrite_lowercase ~loc:exprLoc tag_name args children =
let loc = exprLoc in
let dom_node_name = estring ~loc:exprLoc tag_name in
let key =
args |> List.find_opt ~f:(fun (label, _) -> get_label label = "key") |> Option.map (fun (_, value) -> value)
in
let props = transform_lowercase_props ~loc:exprLoc ~tag_name args in
match (key, children) with
| Some key, Some children ->
let childrens = pexp_list ~loc children in
[%expr React.createElementWithKey ~key:[%e key] [%e dom_node_name] [%e props] [%e childrens]]
| None, Some children ->
let childrens = pexp_list ~loc children in
[%expr React.createElement [%e dom_node_name] [%e props] [%e childrens]]
| Some key, None -> [%expr React.createElementWithKey ~key:[%e key] [%e dom_node_name] [%e props] []]
| None, None -> [%expr React.createElement [%e dom_node_name] [%e props] []]
let split_args args =
let children = ref (Location.none, []) in
let rest =
List.filter_map args ~f:(function
| Labelled "children", children_expression ->
let children' = unwrap_children [] children_expression in
children := (children_expression.pexp_loc, children');
None
| arg_label, e -> Some (arg_label, e))
in
let children_prop = match !children with _loc, [] -> None | _loc, children -> Some children in
(children_prop, rest)
let reverse_pexp_list ~loc expr =
let rec go acc = function
| [%expr []] -> acc
| [%expr [%e? hd] :: [%e? tl]] -> go [%expr [%e hd] :: [%e acc]] tl
| expr -> expr
in
go [%expr []] expr
let list_have_tail expr =
match expr with
| Pexp_construct ({ txt = Lident "::"; _ }, Some { pexp_desc = Pexp_tuple _; _ })
| Pexp_construct ({ txt = Lident "[]"; _ }, None) ->
false
| _ -> true
let transform_items_of_list ~loc children =
let rec run_mapper children accum =
match children with
| [%expr []] -> reverse_pexp_list ~loc accum
| [%expr [%e? v] :: [%e? acc]] when list_have_tail acc.pexp_desc -> [%expr [%e v]]
| [%expr [%e? v] :: [%e? acc]] -> run_mapper acc [%expr [%e v] :: [%e accum]]
| notAList -> notAList
in
run_mapper children [%expr []]
let remove_warning_16_optional_argument_cannot_be_erased ~loc =
let open Ast_helper in
{
attr_name = { txt = "warning"; loc };
attr_payload = PStr [ Str.eval (Exp.constant (Const.string "-16")) ];
attr_loc = loc;
}
let remove_warning_27_unused_var_strict ~loc =
let open Ast_helper in
{
attr_name = { txt = "warning"; loc };
attr_payload = PStr [ Str.eval (Exp.constant (Const.string "-27")) ];
attr_loc = loc;
}
let get_function_name binding =
match binding with
| { pvb_pat = { ppat_desc = Ppat_var { txt } } } -> txt
| _ -> raise_errorf ~loc:binding.pvb_loc "react.component calls cannot be destructured."
let add_unit_at_the_last_argument expression =
let loc = expression.pexp_loc in
let rec inner expression =
match expression.pexp_desc with
| Pexp_fun
(((Labelled _ | Optional _) as label), default, pattern, ({ pexp_desc = Pexp_fun _ } as internalExpression)) ->
pexp_fun ~loc:expression.pexp_loc label default pattern (inner internalExpression)
| Pexp_fun (Nolabel, _, { ppat_desc = Ppat_construct ({ txt = Lident "()" }, _) | Ppat_any }, _) -> expression
| Pexp_fun (label, default, pattern, internalExpression) ->
{
expression with
pexp_attributes = remove_warning_16_optional_argument_cannot_be_erased ~loc :: expression.pexp_attributes;
pexp_desc =
Pexp_fun
(label, default, pattern, pexp_fun ~loc:expression.pexp_loc Nolabel None [%pat? ()] internalExpression);
}
| Pexp_let (recursive, vbs, internalExpression) ->
pexp_let ~loc:expression.pexp_loc recursive vbs (inner internalExpression)
| Pexp_apply (_, [ (Nolabel, internalExpression) ]) -> inner internalExpression
| Pexp_apply (_, [ (Nolabel, internalExpression); ((Nolabel, { pexp_desc = Pexp_fun _ }) as _compareProps) ]) ->
inner internalExpression
| Pexp_sequence (wrapperExpression, internalExpression) ->
pexp_sequence ~loc:expression.pexp_loc wrapperExpression (inner internalExpression)
| _ -> expression
in
inner expression
let transform_fun_body_expression expr fn =
let rec inner expr =
match expr.pexp_desc with
| Pexp_fun (label, def, patt, expression) -> pexp_fun ~loc:expr.pexp_loc label def patt (inner expression)
| _ -> fn expr
in
inner expr
let transform_fun_arguments expr fn =
let rec inner expr =
match expr.pexp_desc with
| Pexp_fun (label, def, patt, expression) -> pexp_fun ~loc:expr.pexp_loc label def (fn patt) (inner expression)
| _ -> expr
in
inner expr
let transform_labelled_arguments_type (core_type : core_type) fn =
let rec inner core_type =
match core_type.ptyp_desc with
| Ptyp_arrow (label, core_type_1, core_type_2) ->
ptyp_arrow ~loc:core_type.ptyp_loc label (fn core_type_1) (inner core_type_2)
| _ -> core_type
in
inner core_type
let expand_make_binding binding react_element_variant_wrapping =
let attributers = binding.pvb_attributes |> List.filter ~f:nonReactAttributes in
let loc = binding.pvb_loc in
let ghost_loc = { binding.pvb_loc with loc_ghost = true } in
let binding_with_unit = add_unit_at_the_last_argument binding.pvb_expr in
let binding_expr = transform_fun_body_expression binding_with_unit react_element_variant_wrapping in
let name = ppat_var ~loc:ghost_loc { txt = get_function_name binding; loc = ghost_loc } in
let key_arg = Optional "key" in
let default_value =
None
in
let underscore = ppat_var ~loc:ghost_loc { txt = "_"; loc } in
let core_type = [%type: string option] in
let key_pattern = ppat_constraint ~loc underscore core_type in
let function_body = pexp_fun ~loc:ghost_loc key_arg default_value key_pattern binding_expr in
{ (value_binding ~loc:ghost_loc ~pat:name ~expr:function_body) with pvb_attributes = attributers }
let get_arguments pvb_expr =
let rec go acc = function
| Pexp_fun (label, default, patt, expr) -> go ((label, default, patt) :: acc) expr.pexp_desc
| _ -> acc
in
go [] pvb_expr.pexp_desc
let make_of_json ~loc (core_type : core_type) prop =
match core_type with
| [%type: React.element] -> [%expr ([%e prop] : React.element)]
| [%type: React.element option] -> [%expr ([%e prop] : React.element option)]
| [%type: [%t? t] Js.Promise.t] -> [%expr ([%e prop] : [%t t] Js.Promise.t)]
| [%type: [%t? t] Runtime.server_function] -> [%expr ([%e prop] : [%t t] Runtime.server_function)]
| [%type: [%t? inner_type] option] as type_ -> (
match inner_type.ptyp_desc with
| Ptyp_arrow (_, _, _) -> [%expr ([%e prop] : [%t type_])]
| _ -> [%expr [%of_json: [%t type_]] [%e prop]])
| type_ -> (
match type_.ptyp_desc with
| Ptyp_arrow (_, _, _) -> [%expr ([%e prop] : [%t type_])]
| _ -> [%expr [%of_json: [%t type_]] [%e prop]])
let props_of_model ~loc (props : (arg_label * expression option * pattern) list) : (longident loc * expression) list =
List.filter_map
~f:(fun (arg_label, default, pattern) ->
match pattern.ppat_desc with
| Ppat_construct ({ txt = Lident "()"; _ }, None) -> None
| Ppat_constraint (_, core_type) -> (
match arg_label with
| Nolabel ->
let loc = pattern.ppat_loc in
Some (longident ~loc "error", [%expr [%ocaml.error "props need to be labelled arguments"]])
| Labelled label | Optional label ->
let core_type = match default with Some _ -> [%type: [%t core_type] option] | None -> core_type in
let prop = [%expr props##[%e ident ~loc label]] in
let value = make_of_json ~loc core_type prop in
Some (longident ~loc label, value))
| _ ->
let loc = pattern.ppat_loc in
let expr =
match arg_label with
| Nolabel -> [%expr [%ocaml.error "server-reason-react: client components need type annotations"]]
| Labelled label | Optional label ->
let msg =
Printf.sprintf
"server-reason-react: client components need type annotations. Missing annotation for '%s'" label
in
let msg_expr = estring ~loc msg in
[%expr [%ocaml.error [%e msg_expr]]]
in
Some (longident ~loc "error", expr))
props
let react_component_attribute ~loc =
{ attr_name = { txt = "react.component"; loc }; attr_payload = PStr []; attr_loc = loc }
let mel_obj ~loc fields =
match fields with
| [] -> [%expr Js.Obj.empty ()]
| _ ->
let record = pexp_record ~loc fields None in
let stri = pstr_eval ~loc record [] in
[%expr [%mel.obj [%%i stri]]]
let expand_make_binding_to_client binding =
let loc = binding.pvb_loc in
let ghost_loc = { binding.pvb_loc with loc_ghost = true } in
let arguments = get_arguments binding.pvb_expr in
let props_as_object_with_decoders = mel_obj ~loc (props_of_model ~loc arguments) in
let make_argument = [ (Nolabel, props_as_object_with_decoders) ] in
let make_call = pexp_apply ~loc:ghost_loc [%expr make] make_argument in
let name = ppat_var ~loc:ghost_loc { txt = "make_client"; loc = ghost_loc } in
let client_single_argument = ppat_var ~loc:ghost_loc { txt = "props"; loc } in
let function_body = pexp_fun ~loc:ghost_loc Nolabel None client_single_argument make_call in
value_binding ~loc:ghost_loc ~pat:name ~expr:function_body
let rec add_unit_at_the_last_argument_in_core_type core_type =
match core_type.ptyp_desc with
| Ptyp_arrow (arg_label, core_type_1, core_type_2) ->
{
core_type with
ptyp_desc = Ptyp_arrow (arg_label, core_type_1, add_unit_at_the_last_argument_in_core_type core_type_2);
}
| Ptyp_constr _ ->
let loc = core_type.ptyp_loc in
{ core_type with ptyp_desc = Ptyp_arrow (Nolabel, [%type: unit], core_type) }
| _ -> core_type
let rewrite_signature_item signature_item =
match signature_item with
| {
psig_loc = _;
psig_desc = Psig_value ({ pval_name = { txt = _fnName }; pval_attributes; pval_type } as psig_desc);
} as psig -> (
let new_ptyp_desc =
match pval_type.ptyp_desc with
| Ptyp_arrow (arg_label, core_type_1, core_type_2) ->
let loc = pval_type.ptyp_loc in
let original_core_type = { pval_type with ptyp_desc = Ptyp_arrow (arg_label, core_type_1, core_type_2) } in
let new_core_type = add_unit_at_the_last_argument_in_core_type original_core_type in
Ptyp_arrow (Optional "key", [%type: string], new_core_type)
| ptyp_desc -> ptyp_desc
in
let new_core_type = { pval_type with ptyp_desc = new_ptyp_desc } in
match List.filter ~f:hasAnyReactComponentAttribute pval_attributes with
| [] -> signature_item
| [ _ ] ->
{
psig with
psig_desc =
Psig_value
{
psig_desc with
pval_type = new_core_type;
pval_attributes = List.filter ~f:nonReactAttributes pval_attributes;
};
}
| _ ->
let loc = signature_item.psig_loc in
[%sigi:
[%%ocaml.error "server-reason-react: there's seems to be an error in the signature of the component."]])
| _ -> signature_item
let make_to_json ~loc (core_type : core_type) prop =
match core_type with
| [%type: React.element] -> [%expr React.Model.Element ([%e prop] : React.element)]
| [%type: React.element option] ->
[%expr
match [%e prop] with Some prop -> React.Model.Element (prop : React.element) | None -> React.Model.Json `Null]
| [%type: [%t? inner_type] Js.Promise.t] ->
let json = [%expr [%to_json: [%t inner_type]]] in
[%expr React.Model.Promise ([%e prop], [%e json])]
| [%type: [%t? inner_type] Js.Promise.t option] ->
let json = [%expr [%to_json: [%t inner_type]]] in
[%expr
match [%e prop] with
| Some prop -> [%expr React.Model.Promise ([%e prop], [%e json])]
| None -> React.Model.Json `Null]
| { ptyp_desc = Ptyp_arrow (_, _, _) } ->
let loc = core_type.ptyp_loc in
[%expr
[%ocaml.error
"server-reason-react: you can't pass functions into client components. Functions aren't serialisable to JSON."]]
| [%type: [%t? _] Runtime.server_function] -> [%expr React.Model.Function [%e prop]]
| type_ ->
let json = [%expr [%to_json: [%t type_]] [%e prop]] in
[%expr React.Model.Json [%e json]]
let props_to_model ~loc (props : (arg_label * expression option * pattern) list) =
List.fold_left ~init:[%expr []]
~f:(fun acc (arg_label, _default, pattern) ->
match pattern.ppat_desc with
| Ppat_construct ({ txt = Lident "()"; _ }, None) -> acc
| Ppat_constraint (_, core_type) -> (
match arg_label with
| Nolabel ->
let loc = pattern.ppat_loc in
[%expr [%ocaml.error "props need to be labelled arguments"] :: [%e acc]]
| Labelled label | Optional label ->
let prop = ident ~loc label in
let value = make_to_json ~loc core_type prop in
let name = estring ~loc label in
[%expr ([%e name], [%e value]) :: [%e acc]])
| _ ->
let loc = pattern.ppat_loc in
let expr =
match arg_label with
| Nolabel -> [%expr [%ocaml.error "server-reason-react: client components need type annotations"]]
| Labelled label | Optional label ->
let msg =
Printf.sprintf
"server-reason-react: client components need type annotations. Missing annotation for '%s'" label
in
let msg_expr = estring ~loc msg in
[%expr [%ocaml.error [%e msg_expr]]]
in
[%expr [%e expr] :: [%e acc]])
props
module ServerFunction = struct
let rec last_expr_to_fn ~loc expr fn =
match expr.pexp_desc with
| Pexp_constraint (expr, _) -> last_expr_to_fn ~loc expr fn
| Pexp_fun (arg_label, arg_expression, fun_pattern, expression) ->
pexp_fun ~loc arg_label arg_expression fun_pattern (last_expr_to_fn ~loc expression fn)
| _ -> fn
let generate_id ~loc name =
let file_path = loc.loc_start.pos_fname in
let replacement =
match shared_folder_prefix.contents with
| Some x ->
if match_substring file_path x then x
else raise_errorf ~loc "Prefix doesn't match the file path. Provide a prefix that matches the file path."
| None -> raise_errorf ~loc "Found a server.function without --shared-folder-prefix argument. Provide one."
in
let file_path = Str.replace_first (Str.regexp replacement) "" file_path in
let hash = Printf.sprintf "%s_%s_%d" name file_path loc.loc_start.pos_lnum |> Hashtbl.hash |> string_of_int in
hash
let get_arg_details (arg : arg_label * expression option * pattern) =
let arg_label, default, pattern = arg in
let loc = pattern.ppat_loc in
match pattern.ppat_desc with
| Ppat_construct ({ txt = Lident "()"; loc }, None) -> Ok (Nolabel, None, [%type: unit])
| Ppat_constraint (pattern, core_type) -> (
let loc = pattern.ppat_loc in
let core_type = match default with Some _ -> [%type: [%t core_type] option] | None -> core_type in
match pattern.ppat_desc with
| Ppat_var { txt = label; _ } -> Ok (arg_label, Some label, core_type)
| _ -> Error (loc, "server-reason-react: server function arguments must have a name"))
| _ -> Error (loc, "server-reason-react: server function arguments must have type annotations")
let get_response_type expr =
let rec aux expr acc =
match expr.pexp_desc with
| Pexp_fun (_, _, _, body) -> aux body acc
| Pexp_constraint (expr, core_type) -> aux expr (Some core_type)
| _ -> acc
in
aux expr None
let response_to_json ~loc core_type response =
match core_type with
| Some [%type: [%t? core_type] Js.Promise.t] ->
let json = [%expr [%to_json: [%t core_type]] [%e response]] in
[%expr React.Model.Json [%e json]]
| Some _ -> [%expr [%ocaml.error "server-reason-react: server functions must return a promise"]]
| _ ->
[%expr [%ocaml.error "server-reason-react: server functions must have a return type annotation (Js.Promise.t)"]]
let map_arguments_to_expressions ~loc args =
List.map
~f:(fun arg ->
match arg with
| Ok (arg_label, Some arg_name, _) -> (arg_label, [%expr [%e evar ~loc arg_name]])
| Ok (arg_label, _, [%type: unit]) -> (arg_label, [%expr ()])
| Ok _ ->
( Nolabel,
[%expr
[%ocaml.error
"server-reason-react: invalid argument, it must have a argument with name and type annotation"]] )
| Error (loc, msg) -> (Nolabel, [%expr [%ocaml.error [%e estring ~loc msg]]]))
args
let encode_function_response ~loc ~response_expr ~core_type =
[%expr
try [%e response_expr] |> Lwt.map (fun response -> [%e response_to_json ~loc core_type [%expr response]])
with e -> Lwt.fail e]
let decode_arguments_vb ~loc args_to_decode =
args_to_decode
|> List.mapi ~f:(fun i (_, label, core_type) ->
let string_of_core_type x =
let f = Format.str_formatter in
Astlib.Pprintast.core_type f x;
Format.flush_str_formatter ()
in
let core_type_string = string_of_core_type core_type in
let of_json = make_of_json ~loc core_type [%expr Stdlib.Array.unsafe_get args [%e eint ~loc i]] in
value_binding ~loc
~pat:[%pat? [%p ppat_var ~loc { txt = label; loc }]]
~expr:
[%expr
try [%e of_json]
with _ ->
Stdlib.raise
(Invalid_argument
(Stdlib.Printf.sprintf
"server-reason-react: error on decoding argument '%s'. EXPECTED: %s, RECEIVED: %s"
[%e estring ~loc label] [%e estring ~loc core_type_string]
(Stdlib.Array.unsafe_get args [%e eint ~loc i] |> Yojson.Basic.to_string)))])
let create_function_reference_registration ~loc ~id ~function_name ~args ~core_type =
let apply_args = map_arguments_to_expressions ~loc args in
let response_expr = pexp_apply ~loc [%expr [%e evar ~loc function_name].call] apply_args in
let encoded_response_expr = encode_function_response ~loc ~response_expr ~core_type in
let args_to_decode =
List.filter_map
~f:(fun arg ->
match arg with
| Ok (_, _, [%type: Js.FormData.t]) -> None
| Ok (arg_label, Some arg_name, core_type) -> Some (arg_label, arg_name, core_type)
| Ok _ -> None
| Error _ -> None)
args
in
let args, formData =
List.partition_map
~f:(fun arg ->
match arg with Ok (_, _, [%type: Js.FormData.t]) -> Right arg | Ok _ -> Left arg | Error _ -> Left arg)
args
in
let body_expr =
match args_to_decode with
| [] -> encoded_response_expr
| args_to_decode ->
let decoded_expr = decode_arguments_vb ~loc args_to_decode in
pexp_let ~loc Nonrecursive decoded_expr encoded_response_expr
in
match (formData, args) with
| [], _ -> [%stri FunctionReferences.register [%e estring ~loc id] (Body (fun args -> [%e body_expr]))]
| [ _ ], [] ->
[%stri FunctionReferences.register [%e estring ~loc id] (FormData (fun _ formData -> [%e body_expr]))]
| _, [] ->
[%stri [%ocaml.error "server-reason-react: server functions with form data must have at only one argument"]]
| _ -> [%stri FunctionReferences.register [%e estring ~loc id] (FormData (fun args formData -> [%e body_expr]))]
let create_server_function_record ~loc id expression =
[%expr { Runtime.id = [%e estring ~loc id]; call = [%e expression] }]
let rewrite_native_function ~vb ~rec_flag structure_item =
let loc = structure_item.pstr_loc in
let function_name = get_function_name vb in
let args = get_arguments vb.pvb_expr |> List.map ~f:get_arg_details |> List.rev in
let base_fn = vb.pvb_expr in
let return_core_type = get_response_type base_fn in
let id = generate_id ~loc:vb.pvb_loc function_name in
let server_function_record_vb =
value_binding ~loc:vb.pvb_loc ~pat:vb.pvb_pat ~expr:(create_server_function_record ~loc:vb.pvb_loc id base_fn)
in
let stri =
[%stri
include struct
[%%i pstr_value ~loc rec_flag [ server_function_record_vb ]]
[%%i create_function_reference_registration ~loc ~id ~function_name ~args ~core_type:return_core_type]
end]
in
stri
let response_of_json ~loc core_type response =
match core_type with
| Some [%type: [%t? core_type] Js.Promise.t] -> [%expr [%of_json: [%t core_type]] [%e response]]
| Some _ -> [%expr [%ocaml.error "server-reason-react: server functions must return a promise"]]
| _ ->
[%expr [%ocaml.error "server-reason-react: server functions must have a return type annotation (Js.Promise.t)"]]
let create_client_function ~loc ~return_core_type id args =
let decode_response = response_of_json ~loc return_core_type in
let apply_args = map_arguments_to_expressions ~loc args |> List.map ~f:(fun (_, expr) -> (Nolabel, expr)) in
let fn =
[%expr
let action = ReactServerDOMEsbuild.createServerReference [%e estring ~loc id] in
([%e pexp_apply ~loc [%expr action] apply_args] [@u])
|> Js.Promise.then_ (fun response -> Js.Promise.resolve [%e decode_response [%expr response]])]
in
fn
let rewrite_client_function ~nested_module_names ~vb ~rec_flag structure_item =
let loc = structure_item.pstr_loc in
let function_name = get_function_name vb in
let args = get_arguments vb.pvb_expr |> List.map ~f:get_arg_details |> List.rev in
let base_fn = vb.pvb_expr in
let return_core_type = get_response_type base_fn in
let id = generate_id ~loc:vb.pvb_loc function_name in
let server_function_record_vb =
value_binding ~loc:vb.pvb_loc ~pat:vb.pvb_pat
~expr:
(create_server_function_record ~loc:vb.pvb_loc id
(last_expr_to_fn ~loc base_fn (create_client_function ~loc ~return_core_type id args)))
in
let loc = structure_item.pstr_loc in
let module_name = String.concat "." nested_module_names in
let _, formData =
List.partition_map
~f:(fun arg ->
match arg with Ok (_, _, [%type: Js.FormData.t]) -> Right arg | Ok _ -> Left arg | Error _ -> Left arg)
args
in
let functionToCall = match formData with [] -> function_name | _ -> Printf.sprintf "%s.call" function_name in
let = Printf.sprintf "// extract-server-function %s %s %s" id functionToCall module_name in
let raw = estring ~loc comment in
let = [%stri [%%raw [%e raw]]] in
[%stri
include struct
[%%i extract_client_raw]
[%%i pstr_value ~loc:structure_item.pstr_loc rec_flag [ server_function_record_vb ]]
end]
end
let rewrite_structure_item ~nested_module_names structure_item =
match structure_item.pstr_desc with
| Pstr_primitive ({ pval_name = { txt = _fnName }; pval_attributes; pval_type = _ } as _value_description) -> (
match
List.filter
~f:(fun attr -> hasAttr attr react_dot_component || hasAttr attr react_dot_async_dot_component)
pval_attributes
with
| [] -> structure_item
| _ ->
let loc = structure_item.pstr_loc in
[%stri
[%%ocaml.error
"externals aren't supported on server-reason-react. externals are used to bind to React components defined \
in JavaScript, in the server, that doesn't make sense. If you need to render this on the server, \
implement a placeholder or an empty element"]])
| Pstr_value (rec_flag, value_bindings) when isReactServerFunctionBinding (List.hd value_bindings) ->
let vb = List.hd value_bindings in
let loc = structure_item.pstr_loc in
if List.length value_bindings > 1 then
[%stri
[%%ocaml.error
"server-reason-react: server functions don't support recursive bindings yet. If you need it, please open an \
issue on https://github.com/reasonml-community/server-reason-react/issues"]]
else ServerFunction.rewrite_native_function ~vb ~rec_flag structure_item
| Pstr_value (rec_flag, value_bindings) ->
let map_value_binding vb =
if isReactClientComponentBinding vb then
expand_make_binding vb (fun expr ->
let loc = expr.pexp_loc in
let fileName = expr.pexp_loc.loc_start.pos_fname in
let replacement =
match shared_folder_prefix.contents with
| Some prefix ->
if match_substring fileName prefix then prefix
else
raise_errorf ~loc
"Prefix doesn't match the file path. Provide a prefix that matches the file path."
| None ->
raise_errorf ~loc
"Found a react.client.component without --shared-folder-prefix argument. Provide one."
in
let file = fileName |> Str.replace_first (Str.regexp replacement) "" |> estring ~loc in
let import_module =
match nested_module_names with
| [] -> file
| _ ->
let submodule = estring ~loc (String.concat "." nested_module_names) in
[%expr Printf.sprintf "%s#%s" [%e file] [%e submodule]]
in
let arguments = get_arguments vb.pvb_expr in
let props = props_to_model ~loc arguments in
[%expr
React.Client_component
{
import_module = [%e import_module];
import_name = "";
props = [%e props];
client = React.Upper_case_component (Stdlib.__FUNCTION__, fun () -> [%e expr]);
}])
else if isReactComponentBinding vb then
expand_make_binding vb (fun expr ->
let loc = expr.pexp_loc in
[%expr React.Upper_case_component (Stdlib.__FUNCTION__, fun () -> [%e expr])])
else if isReactAsyncComponentBinding vb then
expand_make_binding vb (fun expr ->
let loc = expr.pexp_loc in
[%expr React.Async_component (Stdlib.__FUNCTION__, fun () -> [%e expr])])
else vb
in
let bindings = List.map ~f:map_value_binding value_bindings in
pstr_value ~loc:structure_item.pstr_loc rec_flag bindings
| _ -> structure_item
let rewrite_structure_item_for_js ~nested_module_names ctx structure_item =
match structure_item.pstr_desc with
| Pstr_primitive ({ pval_name = { txt = _fnName }; pval_attributes; pval_type = _ } as _value_description) -> (
match List.filter ~f:(fun attr -> hasAttr attr react_dot_client_dot_component) pval_attributes with
| [] -> structure_item
| _ ->
let loc = structure_item.pstr_loc in
[%stri [%%ocaml.error "server-reason-react: externals aren't supported on client components yet"]])
| Pstr_value (rec_flag, value_bindings) when isReactServerFunctionBinding (List.hd value_bindings) ->
let vb = List.hd value_bindings in
ServerFunction.rewrite_client_function ~nested_module_names ~vb ~rec_flag structure_item
| Pstr_value (rec_flag, value_bindings) when isClientComponentBinding value_bindings ->
let first_value_binding = List.hd value_bindings in
let make_client = expand_make_binding_to_client first_value_binding in
let make_client_binding = pstr_value ~loc:structure_item.pstr_loc rec_flag [ make_client ] in
let original_value_binding =
{ first_value_binding with pvb_attributes = [ react_component_attribute ~loc:first_value_binding.pvb_loc ] }
in
let loc = structure_item.pstr_loc in
let code_path = Expansion_context.Base.code_path ctx in
let fileName = Code_path.file_path code_path in
let replacement =
match shared_folder_prefix.contents with
| Some prefix ->
if match_substring fileName prefix then prefix
else raise_errorf ~loc "Prefix doesn't match the file path. Provide a prefix that matches the file path."
| None ->
raise_errorf ~loc "Found a react.client.component without --shared-folder-prefix argument. Provide one."
in
let fileName = Str.replace_first (Str.regexp replacement) "" fileName in
let =
match nested_module_names with
| [] -> estring ~loc (Printf.sprintf "// extract-client %s" fileName)
| _ -> estring ~loc (Printf.sprintf "// extract-client %s %s" fileName (String.concat "." nested_module_names))
in
[%stri
include struct
[%%i [%stri [%%raw [%e comment]]]]
[%%i pstr_value ~loc:structure_item.pstr_loc rec_flag [ original_value_binding ]]
[%%i make_client_binding]
end]
| _ -> structure_item
let validate_tag_children tag children attributes : (unit, string) result =
match Html.is_self_closing_tag tag with
| true when Option.fold ~none:false ~some:(fun children -> List.length children > 0) children ->
Error (Printf.sprintf {|"%s" is a self-closing tag and must not have "children".\n|} tag)
| true
when List.exists
~f:(fun (arg_label, _) ->
match arg_label with
| Labelled "dangerouslySetInnerHTML" | Optional "dangerouslySetInnerHTML" -> true
| _ -> false)
attributes ->
Error (Printf.sprintf {|server-reason-react: "%s" is a self-closing tag and must not have "children".\n|} tag)
| false -> Ok ()
| true -> Ok ()
let traverse =
object (_)
inherit [Expansion_context.Base.t] Ast_traverse.map_with_context as super
val mutable nested_module_names = []
method! module_binding ctxt module_binding =
(match module_binding.pmb_name.txt with
| None -> ()
| Some name -> nested_module_names <- nested_module_names @ [ name ]);
let mapped = super#module_binding ctxt module_binding in
let rec remove_last l = match l with [] -> [] | [ _ ] -> [] | hd :: tl -> hd :: remove_last tl in
nested_module_names <- remove_last nested_module_names;
mapped
method! structure_item ctx structure_item =
match mode.contents with
| Native -> rewrite_structure_item ~nested_module_names (super#structure_item ctx structure_item)
| Js -> rewrite_structure_item_for_js ~nested_module_names ctx (super#structure_item ctx structure_item)
method! signature_item ctx signature_item =
match mode.contents with
| Native -> rewrite_signature_item (super#signature_item ctx signature_item)
| Js -> super#signature_item ctx signature_item
method! expression ctx expr =
let expr = super#expression ctx expr in
let attributes = expr.pexp_attributes in
match mode.contents with
| Js -> (
try
match expr.pexp_desc with
| Pexp_apply (({ pexp_desc = Pexp_ident _; pexp_loc = loc; _ } as tag), args)
when has_jsx_attr expr.pexp_attributes ->
let new_args = Expand_styles_attribute.make ~loc args in
{ (pexp_apply ~loc (super#expression ctx tag) new_args) with pexp_attributes = attributes }
| _ -> expr
with Error err -> [%expr [%e err]])
| Native -> (
try
match expr.pexp_desc with
| Pexp_apply (({ pexp_desc = Pexp_ident _; pexp_loc = loc; _ } as tag), args)
when has_jsx_attr expr.pexp_attributes -> (
let children, rest_of_args = split_args args in
match validate_tag_children (Pprintast.string_of_expression tag) children rest_of_args with
| Error err -> [%expr [%ocaml.error [%e estring ~loc:expr.pexp_loc err]]]
| Ok () -> (
match tag.pexp_desc with
| Pexp_ident { txt = Lident name; loc = _name_loc } ->
let new_args = Expand_styles_attribute.make ~loc rest_of_args in
rewrite_lowercase ~loc:expr.pexp_loc name new_args children
| Pexp_ident { txt = Ldot (modulePath, ("createElement" | "make")); loc } ->
let id = { loc; txt = Ldot (modulePath, "make") } in
rewrite_component ~loc:expr.pexp_loc id rest_of_args children
| Pexp_ident id -> rewrite_component ~loc:expr.pexp_loc id rest_of_args children
| _ -> assert false))
| Pexp_apply (tag, _props) when has_jsx_attr expr.pexp_attributes ->
raise_errorf ~loc:expr.pexp_loc "jsx: %s should be an identifier, not an expression"
(Pprintast.string_of_expression tag)
| Pexp_construct ({ txt = Lident "::"; loc }, Some { pexp_desc = Pexp_tuple _; _ })
| Pexp_construct ({ txt = Lident "[]"; loc }, None) -> (
let jsx_attr, rest_attributes = List.partition ~f:is_jsx expr.pexp_attributes in
match (jsx_attr, rest_attributes) with
| [], _ -> expr
| _, rest_attributes ->
let children = transform_items_of_list ~loc expr in
let new_expr = [%expr React.fragment (React.list [%e children])] in
{ new_expr with pexp_attributes = rest_attributes })
| _ -> expr
with Error err -> [%expr [%e err]])
end
let () =
Driver.add_arg "-melange" (Unit (fun () -> mode := Js)) ~doc:"preprocess for js build";
Driver.add_arg "-shared-folder-prefix"
(String
(fun str ->
let components = String.split_on_char '/' str |> List.filter ~f:(fun x -> x <> "") in
let prefix = String.concat "/" components in
let prefix = if prefix = "" then "" else prefix ^ "/" in
shared_folder_prefix := Some prefix))
~doc:"prefix of shared folder, used to replace the it in the file path";
Ppxlib.Driver.V2.register_transformation "server-reason-react.ppx" ~preprocess_impl:traverse#structure
~preprocess_intf:traverse#signature