12
12
(*
13
13
The actual transform:
14
14
15
- transform `div props1::a props2::b children::[foo, bar] () [@JSX]` into
16
- `ReactDOMRe.createElement "div" props::[%bs.obj {props1: 1, props2: b}] [|foo,
17
- bar|]`.
15
+ transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into
16
+ `ReactDOMRe.createElement("div", ~props={"props1": 1, "props2": b}, [|foo,
17
+ bar|])`.
18
+
19
+ transform `[@JSX] div(~props1=a, ~props2=b, ~children=foo, ())` into
20
+ `ReactDOMRe.createElementVariadic("div", ~props={"props1": 1, "props2": b}, foo)`.
18
21
19
22
transform the upper-cased case
20
- `Foo.createElement key::a ref::b foo::bar children::[] () [@JSX]` into
21
- `ReasonReact.element key::a ref::b (Foo.make foo::bar [||] [@JSX])`
23
+ `[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into
24
+ `ReasonReact.element(~key=a, ~ref=b, Foo.make(~foo=bar, [||]))`
25
+
26
+ transform `[@JSX] [foo]` into
27
+ `ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
22
28
*)
23
29
24
30
(*
@@ -51,8 +57,8 @@ open Parsetree
51
57
open Longident
52
58
53
59
(* if children is a list, convert it to an array while mapping each element. If not, just map over it, as usual *)
54
- let transformChildren ~loc ~mapper theList =
55
- let rec transformChildren ' theList accum =
60
+ let transformChildrenIfList ~loc ~mapper theList =
61
+ let rec transformChildren_ theList accum =
56
62
(* not in the sense of converting a list to an array; convert the AST
57
63
reprensentation of a list to the AST reprensentation of an array *)
58
64
match theList with
@@ -62,12 +68,12 @@ let transformChildren ~loc ~mapper theList =
62
68
{txt = Lident " ::" },
63
69
Some {pexp_desc = Pexp_tuple (v::acc::[] )}
64
70
)} ->
65
- transformChildren' acc ((mapper.expr mapper v)::accum)
71
+ transformChildren_ acc ((mapper.expr mapper v)::accum)
66
72
| notAList -> mapper.expr mapper notAList
67
73
in
68
- transformChildren' theList []
74
+ transformChildren_ theList []
69
75
70
- let extractChildrenForDOMElements ?(removeLastPositionUnit =false ) ~loc propsAndChildren =
76
+ let extractChildren ?(removeLastPositionUnit =false ) ~loc propsAndChildren =
71
77
let rec allButLast_ lst acc = match lst with
72
78
| [] -> []
73
79
@@ -78,56 +84,25 @@ let extractChildrenForDOMElements ?(removeLastPositionUnit=false) ~loc propsAndC
78
84
in
79
85
let allButLast lst = allButLast_ lst [] |> List. rev in
80
86
match (List. partition (fun (label , expr ) -> label = labelled " children" ) propsAndChildren) with
81
- | ((label , childrenExpr )::[] , props ) ->
82
- (childrenExpr, if removeLastPositionUnit then allButLast props else props)
83
87
| ([] , props ) ->
84
- (* no children provided? Place a placeholder list (don't forgot we're talking about DOM element conversion here only) *)
88
+ (* no children provided? Place a placeholder list *)
85
89
(Exp. construct ~loc {loc; txt = Lident " []" } None , if removeLastPositionUnit then allButLast props else props)
90
+ | ([(label , childrenExpr )], props ) ->
91
+ (childrenExpr, if removeLastPositionUnit then allButLast props else props)
86
92
| (moreThanOneChild , props ) -> raise (Invalid_argument " JSX: somehow there's more than one `children` label" )
87
93
88
94
(* TODO: some line number might still be wrong *)
89
95
let jsxMapper () =
90
96
91
- let jsxTransformV3 modulePath mapper loc attrs callExpression callArguments =
92
- let (children, argsWithLabels) =
93
- extractChildrenForDOMElements ~loc ~remove LastPositionUnit:true callArguments in
94
- let (argsKeyRef, argsForMake) = List. partition argIsKeyRef argsWithLabels in
95
- let childrenExpr = match children with
96
- (* if it's a single, non-jsx item, keep it so (remove the list wrapper, don't add the array wrapper) *)
97
- | {pexp_desc = Pexp_construct (
98
- {txt = Lident " ::" ; loc},
99
- Some {pexp_desc = Pexp_tuple [
100
- ({pexp_attributes} as singleItem);
101
- {pexp_desc = Pexp_construct ({txt = Lident " []" }, None )}
102
- ]}
103
- )} when List. for_all (fun (attribute , _ ) -> attribute.txt <> " JSX" ) pexp_attributes ->
104
- mapper.expr mapper singleItem
105
- (* if it's a single jsx item, or multiple items, turn list into an array *)
106
- | nonEmptyChildren -> transformChildren ~loc ~mapper nonEmptyChildren
107
- in
108
- let recursivelyTransformedArgsForMake = argsForMake |> List. map (fun (label , expression ) -> (label, mapper.expr mapper expression)) in
109
- let args = recursivelyTransformedArgsForMake @ [ (nolabel, childrenExpr) ] in
110
- let wrapWithReasonReactElement e = (* ReasonReact.element ::key ::ref (...) *)
111
- Exp. apply
112
- ~loc
113
- (Exp. ident ~loc {loc; txt = Ldot (Lident " ReasonReact" , " element" )})
114
- (argsKeyRef @ [(nolabel, e)]) in
115
- Exp. apply
116
- ~loc
117
- ~attrs
118
- (* Foo.make *)
119
- (Exp. ident ~loc {loc; txt = Ldot (modulePath, " make" )})
120
- args
121
- |> wrapWithReasonReactElement in
97
+ let jsxVersion = ref None in
122
98
123
- let jsxTransformV2 modulePath mapper loc attrs callExpression callArguments =
124
- let (children, argsWithLabels) =
125
- extractChildrenForDOMElements ~loc ~remove LastPositionUnit:true callArguments in
99
+ let transformUppercaseCall modulePath mapper loc attrs callExpression callArguments =
100
+ let (children, argsWithLabels) = extractChildren ~loc ~remove LastPositionUnit:true callArguments in
126
101
let (argsKeyRef, argsForMake) = List. partition argIsKeyRef argsWithLabels in
127
- let childrenExpr = transformChildren ~loc ~mapper children in
102
+ let childrenExpr = transformChildrenIfList ~loc ~mapper children in
128
103
let recursivelyTransformedArgsForMake = argsForMake |> List. map (fun (label , expression ) -> (label, mapper.expr mapper expression)) in
129
104
let args = recursivelyTransformedArgsForMake @ [ (nolabel, childrenExpr) ] in
130
- let wrapWithReasonReactElement e = (* ReasonReact.element :: key :: ref ( ...) *)
105
+ let wrapWithReasonReactElement e = (* ReasonReact.element(~ key, ~ ref, ...) *)
131
106
Exp. apply
132
107
~loc
133
108
(Exp. ident ~loc {loc; txt = Ldot (Lident " ReasonReact" , " element" )})
@@ -140,13 +115,36 @@ let jsxMapper () =
140
115
args
141
116
|> wrapWithReasonReactElement in
142
117
143
- let lowercaseCaller mapper loc attrs callArguments id =
144
- let (children, propsWithLabels) =
145
- extractChildrenForDOMElements ~loc callArguments in
118
+ let transformLowercaseCall mapper loc attrs callArguments id =
119
+ let (children, nonChildrenProps) = extractChildren ~loc callArguments in
146
120
let componentNameExpr = constantString ~loc id in
147
- let childrenExpr = transformChildren ~loc ~mapper children in
148
- let args = match propsWithLabels with
149
- | [theUnitArgumentAtEnd] ->
121
+ let childrenExpr = transformChildrenIfList ~loc ~mapper children in
122
+ let createElementCall = match children with
123
+ (* [@JSX] div(~children=[a]), coming from <div> a </div> *)
124
+ | {
125
+ pexp_desc =
126
+ Pexp_construct ({txt = Lident " ::" ; loc}, Some {pexp_desc = Pexp_tuple _ })
127
+ | Pexp_construct ({txt = Lident " []" ; loc}, None );
128
+ pexp_attributes
129
+ } -> " createElement"
130
+ (* [@JSX] div(~children=[|a|]), coming from <div> ...[|a|] </div> *)
131
+ | {
132
+ pexp_desc = (Pexp_array _);
133
+ pexp_attributes
134
+ } ->
135
+ raise (Invalid_argument " A spread + an array literal as a DOM element's \
136
+ children would cancel each other out, and thus don't make sense written \
137
+ together. You can simply remove the spread and the array literal." )
138
+ (* [@JSX] div(~children= <div />), coming from <div> ...<div/> </div> *)
139
+ | {
140
+ pexp_attributes
141
+ } when pexp_attributes |> List. exists (fun (attribute , _ ) -> attribute.txt = " JSX" ) ->
142
+ raise (Invalid_argument " A spread + a JSX literal as a DOM element's \
143
+ children don't make sense written together. You can simply remove the spread." )
144
+ | notAList -> " createElementVariadic"
145
+ in
146
+ let args = match nonChildrenProps with
147
+ | [_justTheUnitArgumentAtEnd] ->
150
148
[
151
149
(* "div" *)
152
150
(nolabel, componentNameExpr);
@@ -163,7 +161,7 @@ let jsxMapper () =
163
161
[
164
162
(* "div" *)
165
163
(nolabel, componentNameExpr);
166
- (* ReactDOMRe.props className: blabla foo:: bar ( ) *)
164
+ (* ReactDOMRe.props(~ className= blabla, ~ foo= bar, () ) *)
167
165
(labelled " props" , propsCall);
168
166
(* [|moreCreateElementCallsHere|] *)
169
167
(nolabel, childrenExpr)
@@ -172,82 +170,31 @@ let jsxMapper () =
172
170
~loc
173
171
(* throw away the [@JSX] attribute and keep the others, if any *)
174
172
~attrs
175
- (* ReactDOMRe.createDOMElement *)
176
- (Exp. ident ~loc {loc; txt = Ldot (Lident " ReactDOMRe" , " createElement" )})
177
- args in
178
-
179
- let jsxVersion = ref None in
180
-
181
- let structure =
182
- (fun mapper structure -> match structure with
183
- (*
184
- match against [@@@bs.config {foo, jsx: ...}] at the file-level. This
185
- indicates which version of JSX we're using. This code stays here because
186
- we used to have 2 versions of JSX PPX (and likely will again in the
187
- future when JSX PPX changes). So the architecture for switching between
188
- JSX behavior stayed here. To create a new JSX ppx, copy paste this
189
- entire file and change the relevant parts.
190
-
191
- Description of architecture: in bucklescript's bsconfig.json, you can
192
- specify a project-wide JSX version. You can also specify a file-level
193
- JSX version. This degree of freedom allows a person to convert a project
194
- one file at time onto the new JSX, when it was released. It also enabled
195
- a project to depend on a third-party which is still using an old version
196
- of JSX
197
- *)
198
- | {
199
- pstr_loc;
200
- pstr_desc = Pstr_attribute (
201
- ({txt = " bs.config" } as bsConfigLabel),
202
- PStr [{pstr_desc = Pstr_eval ({pexp_desc = Pexp_record (recordFields, b)} as innerConfigRecord, a)} as configRecord]
203
- )
204
- }::restOfStructure -> begin
205
- let (jsxField, recordFieldsWithoutJsx) = recordFields |> List. partition (fun ({txt} , _ ) -> txt = Lident " jsx" ) in
206
- match (jsxField, recordFieldsWithoutJsx) with
207
- (* no file-level jsx config found *)
208
- | ([] , _ ) -> default_mapper.structure mapper structure
209
- (* {jsx: 2 | 3} *)
210
-
211
- | ((_ , {pexp_desc = Pexp_constant (Const_int version )} )::rest , recordFieldsWithoutJsx ) -> begin
212
- (match version with
213
- | 2 -> jsxVersion := Some 2
214
- | 3 -> jsxVersion := Some 3
215
- | _ -> raise (Invalid_argument " JSX: the file-level bs.config's jsx version must be either 2 or 3" ));
216
-
217
- match recordFieldsWithoutJsx with
218
- (* record empty now, remove the whole bs.config attribute *)
219
- | [] -> default_mapper.structure mapper restOfStructure
220
- | fields -> default_mapper.structure mapper ({
221
- pstr_loc;
222
- pstr_desc = Pstr_attribute (
223
- bsConfigLabel,
224
- PStr [{configRecord with pstr_desc = Pstr_eval ({innerConfigRecord with pexp_desc = Pexp_record (fields, b)}, a)}]
225
- )
226
- }::restOfStructure)
227
- end
228
- | (_ , recordFieldsWithoutJsx ) -> raise (Invalid_argument " JSX: the file-level bs.config's {jsx: ...} config accepts only a version number" )
229
- end
230
- | _ -> default_mapper.structure mapper structure
231
- ) in
173
+ (* ReactDOMRe.createElement *)
174
+ (Exp. ident ~loc {loc; txt = Ldot (Lident " ReactDOMRe" , createElementCall)})
175
+ args
176
+ in
232
177
233
178
let transformJsxCall mapper callExpression callArguments attrs =
234
179
(match callExpression.pexp_desc with
235
180
| Pexp_ident caller ->
236
181
(match caller with
237
182
| {txt = Lident "createElement" } ->
238
183
raise (Invalid_argument " JSX: `createElement` should be preceeded by a module name." )
239
- (* Foo.createElement prop1::foo prop2:bar children::[] () *)
184
+
185
+ (* Foo.createElement(~prop1=foo, ~prop2=bar, ~children=[], ()) *)
240
186
| {loc; txt = Ldot (modulePath , ("createElement" | "make" ))} ->
241
- let f = match ! jsxVersion with
242
- | Some 2 -> jsxTransformV2 modulePath
243
- | Some 3 -> jsxTransformV3 modulePath
244
- | Some _ -> raise (Invalid_argument " JSX: the JSX version must be either 2 or 3 " )
245
- | None -> jsxTransformV2 modulePath
246
- in f mapper loc attrs callExpression callArguments
247
- (* div prop1::foo prop2:bar children::[bla] () *)
248
- (* turn that into ReactDOMRe.createElement props::( ReactDOMRe.props props1:: foo props2:: bar ()) [|bla|] *)
187
+ ( match ! jsxVersion with
188
+ | None
189
+ | Some 2 -> transformUppercaseCall modulePath mapper loc attrs callExpression callArguments
190
+ | Some _ -> raise (Invalid_argument " JSX: the JSX version must be 2 " ) )
191
+
192
+ (* div(~prop1=foo, ~prop2=bar, ~children=[bla], ()) *)
193
+ (* turn that into
194
+ ReactDOMRe.createElement(~props= ReactDOMRe.props(~ props1= foo, ~ props2= bar, ()), [|bla|]) *)
249
195
| {loc; txt = Lident id } ->
250
- lowercaseCaller mapper loc attrs callArguments id
196
+ transformLowercaseCall mapper loc attrs callArguments id
197
+
251
198
| {txt = Ldot (_ , anythingNotCreateElementOrMake )} ->
252
199
raise (
253
200
Invalid_argument
@@ -256,6 +203,7 @@ let jsxMapper () =
256
203
^ " ` instead"
257
204
)
258
205
)
206
+
259
207
| {txt = Lapply _ } ->
260
208
(* don't think there's ever a case where this is reached *)
261
209
raise (
@@ -268,11 +216,61 @@ let jsxMapper () =
268
216
)
269
217
) in
270
218
219
+ let structure =
220
+ (fun mapper structure -> match structure with
221
+ (*
222
+ match against [@bs.config {foo, jsx: ...}] at the file-level. This
223
+ indicates which version of JSX we're using. This code stays here because
224
+ we used to have 2 versions of JSX PPX (and likely will again in the
225
+ future when JSX PPX changes). So the architecture for switching between
226
+ JSX behavior stayed here. To create a new JSX ppx, copy paste this
227
+ entire file and change the relevant parts.
228
+
229
+ Description of architecture: in bucklescript's bsconfig.json, you can
230
+ specify a project-wide JSX version. You can also specify a file-level
231
+ JSX version. This degree of freedom allows a person to convert a project
232
+ one file at time onto the new JSX, when it was released. It also enabled
233
+ a project to depend on a third-party which is still using an old version
234
+ of JSX
235
+ *)
236
+ | {
237
+ pstr_loc;
238
+ pstr_desc = Pstr_attribute (
239
+ ({txt = " bs.config" } as bsConfigLabel),
240
+ PStr [{pstr_desc = Pstr_eval ({pexp_desc = Pexp_record (recordFields, b)} as innerConfigRecord, a)} as configRecord]
241
+ )
242
+ }::restOfStructure -> begin
243
+ let (jsxField, recordFieldsWithoutJsx) = recordFields |> List. partition (fun ({txt} , _ ) -> txt = Lident " jsx" ) in
244
+ match (jsxField, recordFieldsWithoutJsx) with
245
+ (* no file-level jsx config found *)
246
+ | ([] , _ ) -> default_mapper.structure mapper structure
247
+ (* {jsx: 2} *)
248
+
249
+ | ((_ , {pexp_desc = Pexp_constant (Const_int version )} )::rest , recordFieldsWithoutJsx ) -> begin
250
+ (match version with
251
+ | 2 -> jsxVersion := Some 2
252
+ | _ -> raise (Invalid_argument " JSX: the file-level bs.config's jsx version must be 2" ));
253
+
254
+ match recordFieldsWithoutJsx with
255
+ (* record empty now, remove the whole bs.config attribute *)
256
+ | [] -> default_mapper.structure mapper restOfStructure
257
+ | fields -> default_mapper.structure mapper ({
258
+ pstr_loc;
259
+ pstr_desc = Pstr_attribute (
260
+ bsConfigLabel,
261
+ PStr [{configRecord with pstr_desc = Pstr_eval ({innerConfigRecord with pexp_desc = Pexp_record (fields, b)}, a)}]
262
+ )
263
+ }::restOfStructure)
264
+ end
265
+ | (_ , recordFieldsWithoutJsx ) -> raise (Invalid_argument " JSX: the file-level bs.config's {jsx: ...} config accepts only a version number" )
266
+ end
267
+ | _ -> default_mapper.structure mapper structure
268
+ ) in
269
+
271
270
let expr =
272
271
(fun mapper expression -> match expression with
273
272
(* Does the function application have the @JSX attribute? *)
274
- |
275
- {
273
+ | {
276
274
pexp_desc = Pexp_apply (callExpression, callArguments);
277
275
pexp_attributes
278
276
} ->
@@ -281,6 +279,35 @@ let jsxMapper () =
281
279
(* no JSX attribute *)
282
280
| ([] , _ ) -> default_mapper.expr mapper expression
283
281
| (_ , nonJSXAttributes ) -> transformJsxCall mapper callExpression callArguments nonJSXAttributes)
282
+
283
+ (* is it a list with jsx attribute? Reason <>foo</> desugars to [@JSX][foo]*)
284
+ | {
285
+ pexp_desc =
286
+ Pexp_construct ({txt = Lident " ::" ; loc}, Some {pexp_desc = Pexp_tuple _ })
287
+ | Pexp_construct ({txt = Lident " []" ; loc}, None );
288
+ pexp_attributes
289
+ } as listItems ->
290
+ let (jsxAttribute, nonJSXAttributes) = List. partition (fun (attribute , _ ) -> attribute.txt = " JSX" ) pexp_attributes in
291
+ (match (jsxAttribute, nonJSXAttributes) with
292
+ (* no JSX attribute *)
293
+ | ([] , _ ) -> default_mapper.expr mapper expression
294
+ | (_ , nonJSXAttributes ) ->
295
+ let fragment = Exp. ident ~loc {loc; txt = Ldot (Lident " ReasonReact" , " fragment" )} in
296
+ let childrenExpr = transformChildrenIfList ~loc ~mapper listItems in
297
+ let args = [
298
+ (* "div" *)
299
+ (nolabel, fragment);
300
+ (* [|moreCreateElementCallsHere|] *)
301
+ (nolabel, childrenExpr)
302
+ ] in
303
+ Exp. apply
304
+ ~loc
305
+ (* throw away the [@JSX] attribute and keep the others, if any *)
306
+ ~attrs: nonJSXAttributes
307
+ (* ReactDOMRe.createElement *)
308
+ (Exp. ident ~loc {loc; txt = Ldot (Lident " ReactDOMRe" , " createElement" )})
309
+ args
310
+ )
284
311
(* Delegate to the default mapper, a deep identity traversal *)
285
312
| e -> default_mapper.expr mapper e) in
286
313
@@ -357,3 +384,4 @@ let make_ppx name =
357
384
358
385
let () = make_ppx " jsxv2"
359
386
387
+
0 commit comments