Skip to content
This repository was archived by the owner on Jun 15, 2023. It is now read-only.

Commit 9d2be71

Browse files
authored
Move React ppx from compiler repo, add tests (#124)
* Move React ppx from compiler repo, add tests React ppx copied from: https://github.com/rescript-lang/rescript-compiler/blob/4f4812aa4e1411029392f6dbcacc580e755d68d5/jscomp/syntax/reactjs_jsx_ppx.cppo.ml * Fix roundtrip tests, ignore test files with .fixme suffix * add -ppx cmd line arg * rename .react.res to just .res * remove unneeded jsx stuff from res_multi_printer * remove .fixme option and add previously failing test + snapshot * don't run roundtrip tests if ppx is defined * move ppx tests to own folder, remove "none" as default arg value, cleanup
1 parent ef260b2 commit 9d2be71

13 files changed

+1091
-14
lines changed

.depend

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
src/reactjs_jsx_ppx.cmx : src/reactjs_jsx_ppx.cmi
2+
src/reactjs_jsx_ppx.cmi :
13
src/res_ast_conversion.cmx : src/res_ast_conversion.cmi
24
src/res_ast_conversion.cmi :
35
src/res_ast_debugger.cmx : src/res_driver.cmx src/res_doc.cmx \
@@ -6,7 +8,7 @@ src/res_ast_debugger.cmi : src/res_driver.cmi
68
src/res_character_codes.cmx :
79
src/res_cli.cmx : src/res_driver_reason_binary.cmx \
810
src/res_driver_ml_parser.cmx src/res_driver_binary.cmx src/res_driver.cmx \
9-
src/res_ast_debugger.cmx
11+
src/res_ast_debugger.cmx src/reactjs_jsx_ppx.cmx
1012
src/res_comment.cmx : src/res_comment.cmi
1113
src/res_comment.cmi :
1214
src/res_comments_table.cmx : src/res_parsetree_viewer.cmx src/res_doc.cmx \

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ depend:
1111
$(OCAMLDEP) -native -I tests -I src src/*.ml src/*.mli tests/*.ml tests/*.mli > .depend
1212

1313
API_FILES = \
14+
src/reactjs_jsx_ppx.cmx\
1415
src/res_io.cmx\
1516
src/res_minibuffer.cmx\
1617
src/res_doc.cmx\

src/reactjs_jsx_ppx.ml

Lines changed: 880 additions & 0 deletions
Large diffs are not rendered by default.

src/reactjs_jsx_ppx.mli

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
(*
2+
This is the module that handles turning Reason JSX' agnostic function call into
3+
a ReasonReact-specific function call. Aka, this is a macro, using OCaml's ppx
4+
facilities; https://whitequark.org/blog/2014/04/16/a-guide-to-extension-
5+
points-in-ocaml/
6+
You wouldn't use this file directly; it's used by ReScript's
7+
bsconfig.json. Specifically, there's a field called `react-jsx` inside the
8+
field `reason`, which enables this ppx through some internal call in bsb
9+
*)
10+
11+
(*
12+
There are two different transforms that can be selected in this file (v2 and v3):
13+
v2:
14+
transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into
15+
`ReactDOMRe.createElement("div", ~props={"props1": 1, "props2": b}, [|foo,
16+
bar|])`.
17+
transform `[@JSX] div(~props1=a, ~props2=b, ~children=foo, ())` into
18+
`ReactDOMRe.createElementVariadic("div", ~props={"props1": 1, "props2": b}, foo)`.
19+
transform the upper-cased case
20+
`[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into
21+
`ReasonReact.element(~key=a, ~ref=b, Foo.make(~foo=bar, [||]))`
22+
transform `[@JSX] [foo]` into
23+
`ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
24+
v3:
25+
transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into
26+
`ReactDOMRe.createDOMElementVariadic("div", ReactDOMRe.domProps(~props1=1, ~props2=b), [|foo, bar|])`.
27+
transform the upper-cased case
28+
`[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into
29+
`React.createElement(Foo.make, Foo.makeProps(~key=a, ~ref=b, ~foo=bar, ()))`
30+
transform the upper-cased case
31+
`[@JSX] Foo.createElement(~foo=bar, ~children=[foo, bar], ())` into
32+
`React.createElementVariadic(Foo.make, Foo.makeProps(~foo=bar, ~children=React.null, ()), [|foo, bar|])`
33+
transform `[@JSX] [foo]` into
34+
`ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
35+
*)
36+
37+
val rewrite_implementation : Parsetree.structure -> Parsetree.structure
38+
39+
val rewrite_signature : Parsetree.signature -> Parsetree.signature

src/res_cli.ml

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ module ResClflags: sig
165165
val files: string list ref
166166
val interface: bool ref
167167
val report: string ref
168+
val ppx: string ref
168169

169170
val parse: unit -> unit
170171
end = struct
@@ -178,6 +179,7 @@ end = struct
178179
let origin = ref ""
179180
let interface = ref false
180181
let report = ref "pretty"
182+
let ppx = ref ""
181183

182184
let usage = "Usage:\n rescript <options> <file>\n\n" ^
183185
"Examples:\n" ^
@@ -192,6 +194,7 @@ end = struct
192194
("-print", Arg.String (fun txt -> print := txt), "Print either binary or ns. Default: ns");
193195
("-width", Arg.Int (fun w -> width := w), "Specify the line length for the printer (formatter)");
194196
("-interface", Arg.Unit (fun () -> interface := true), "Parse as interface");
197+
("-ppx", Arg.String (fun txt -> ppx := txt), "Apply a specific built-in ppx before parsing, none or jsx. Default: none");
195198
(* ("-report", Arg.String (fun txt -> report := txt), "Stylize errors and messages using color and context. Accepts `Pretty` and `Plain`. Default `Plain`") *)
196199
]
197200

@@ -201,7 +204,7 @@ end
201204
module CliArgProcessor = struct
202205
type backend = Parser: ('diagnostics) Res_driver.parsingEngine -> backend [@@unboxed]
203206

204-
let processFile ~isInterface ~width ~recover ~origin ~target ~report:_ filename =
207+
let processFile ~isInterface ~width ~recover ~origin ~target ~report:_ ~ppx filename =
205208
try
206209
let len = String.length filename in
207210
let processInterface =
@@ -243,8 +246,12 @@ module CliArgProcessor = struct
243246
else exit 1
244247
end
245248
else
249+
let parsetree = match ppx with
250+
| "jsx" -> Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree
251+
| _ -> parseResult.parsetree
252+
in
246253
printEngine.printInterface
247-
~width ~filename ~comments:parseResult.comments parseResult.parsetree
254+
~width ~filename ~comments:parseResult.comments parsetree
248255
else
249256
let parseResult = backend.parseImplementation ~forPrinter ~filename in
250257
if parseResult.invalid then begin
@@ -258,19 +265,23 @@ module CliArgProcessor = struct
258265
else exit 1
259266
end
260267
else
268+
let parsetree = match ppx with
269+
| "jsx" -> Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree
270+
| _ -> parseResult.parsetree
271+
in
261272
printEngine.printImplementation
262-
~width ~filename ~comments:parseResult.comments parseResult.parsetree
273+
~width ~filename ~comments:parseResult.comments parsetree
263274
with
264275
| Failure txt ->
265276
prerr_string txt;
266277
prerr_newline();
267278
exit 1
268279
| _ -> exit 1
269-
[@@raises exit]
280+
[@@raises Invalid_argument, exit]
270281
end
271282

272283

273-
let [@raises exit] () =
284+
let [@raises Invalid_argument, exit] () =
274285
if not !Sys.interactive then begin
275286
ResClflags.parse ();
276287
match !ResClflags.files with
@@ -282,6 +293,7 @@ let [@raises exit] () =
282293
~target:!ResClflags.print
283294
~origin:!ResClflags.origin
284295
~report:!ResClflags.report
296+
~ppx:!ResClflags.ppx
285297
""
286298
| files ->
287299
List.iter (fun filename ->
@@ -292,6 +304,7 @@ let [@raises exit] () =
292304
~target:!ResClflags.print
293305
~origin:!ResClflags.origin
294306
~report:!ResClflags.report
307+
~ppx:!ResClflags.ppx
295308
filename
296309
) files
297310
end

tests/api/resReactJsx.res

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// test React JSX file
2+
3+
@react.component
4+
let make = (~msg) => {
5+
<div> {msg->React.string} </div>
6+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`commentAtTop.res 1`] = `
4+
"@bs.obj
5+
external makeProps: (~msg: 'msg, ~key: string=?, unit) => {\\"msg\\": 'msg} = \\"\\" // test React JSX file
6+
7+
let make =
8+
(@warning(\\"-16\\") ~msg) => {
9+
ReactDOMRe.createDOMElementVariadic(\\"div\\", [{msg->React.string}])
10+
}
11+
let make = {
12+
let \\\\\\"CommentAtTop\\" = (\\\\\\"Props\\": {\\"msg\\": 'msg}) => make(~msg=\\\\\\"Props\\"[\\"msg\\"])
13+
\\\\\\"CommentAtTop\\"
14+
}
15+
"
16+
`;
17+
18+
exports[`externalWithCustomName.res 1`] = `
19+
"module Foo = {
20+
@bs.obj
21+
external componentProps: (
22+
~a: int,
23+
~b: string,
24+
~key: string=?,
25+
unit,
26+
) => {\\"a\\": int, \\"b\\": string} = \\"\\"
27+
@bs.module(\\"Foo\\")
28+
external component: React.componentLike<
29+
{\\"a\\": int, \\"b\\": string},
30+
React.element,
31+
> = \\"component\\"
32+
}
33+
34+
let t = React.createElement(
35+
Foo.component,
36+
Foo.componentProps(~a=1, ~b={\\"1\\"}, ()),
37+
)
38+
"
39+
`;
40+
41+
exports[`innerModule.res 1`] = `
42+
"module Bar = {
43+
@bs.obj
44+
external makeProps: (
45+
~a: 'a,
46+
~b: 'b,
47+
~key: string=?,
48+
unit,
49+
) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
50+
let make =
51+
(@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => {
52+
Js.log(\\"This function should be named \`InnerModule.react$Bar\`\\")
53+
ReactDOMRe.createDOMElementVariadic(\\"div\\", [])
54+
}
55+
let make = {
56+
let \\\\\\"InnerModule$Bar\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) =>
57+
make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ())
58+
\\\\\\"InnerModule$Bar\\"
59+
}
60+
@bs.obj
61+
external componentProps: (
62+
~a: 'a,
63+
~b: 'b,
64+
~key: string=?,
65+
unit,
66+
) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
67+
68+
let component =
69+
(@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => {
70+
Js.log(\\"This function should be named \`InnerModule.react$Bar$component\`\\")
71+
ReactDOMRe.createDOMElementVariadic(\\"div\\", [])
72+
}
73+
let component = {
74+
let \\\\\\"InnerModule$Bar$component\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) =>
75+
component(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ())
76+
\\\\\\"InnerModule$Bar$component\\"
77+
}
78+
}
79+
"
80+
`;
81+
82+
exports[`topLevel.res 1`] = `
83+
"@bs.obj
84+
external makeProps: (
85+
~a: 'a,
86+
~b: 'b,
87+
~key: string=?,
88+
unit,
89+
) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
90+
let make =
91+
(@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => {
92+
Js.log(\\"This function should be named 'TopLevel.react'\\")
93+
ReactDOMRe.createDOMElementVariadic(\\"div\\", [])
94+
}
95+
let make = {
96+
let \\\\\\"TopLevel\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) =>
97+
make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ())
98+
\\\\\\"TopLevel\\"
99+
}
100+
"
101+
`;

tests/ppx/react/commentAtTop.res

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// test React JSX file
2+
3+
@react.component
4+
let make = (~msg) => {
5+
<div> {msg->React.string} </div>
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module Foo = {
2+
@react.component @bs.module("Foo")
3+
external component: (~a: int, ~b: string, _) => React.element = "component"
4+
}
5+
6+
let t = <Foo.component a=1 b={"1"} />

tests/ppx/react/innerModule.res

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module Bar = {
2+
@react.component
3+
let make = (~a, ~b, _) => {
4+
Js.log(
5+
"This function should be named `InnerModule.react$Bar`",
6+
)
7+
<div />
8+
}
9+
@react.component
10+
let component = (~a, ~b, _) => {
11+
Js.log(
12+
"This function should be named `InnerModule.react$Bar$component`",
13+
)
14+
<div />
15+
}
16+
}

tests/ppx/react/render.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
runPrinter(__dirname, "jsx")

tests/ppx/react/topLevel.res

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@react.component
2+
let make = (~a, ~b, _) => {
3+
Js.log("This function should be named 'TopLevel.react'")
4+
<div />
5+
}

tests/runner.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ function parseNapkinStdinToNapkin(src, isInterface, width = 100) {
151151
.stdout.toString("utf8");
152152
}
153153

154-
function printFile(filename) {
154+
function printFile(filename, ppx) {
155155
let parserSrc;
156156
switch (classifyLang(filename)) {
157157
case "ocaml":
@@ -165,17 +165,15 @@ function printFile(filename) {
165165
status: 0,
166166
errorOutput: ""
167167
};
168-
break;
169-
168+
170169
case "rescript":
171170
default:
172171
parserSrc = "res";
173172
break;
174173
}
175174

176175
let intf = isInterface(filename);
177-
178-
let args = ["-parse", parserSrc, "-print", "res", "-width", "80"];
176+
let args = ["-parse", parserSrc, "-print", "res", "-width", "80", "-ppx", ppx];
179177

180178
if (intf) {
181179
args.push("-interface");
@@ -204,15 +202,16 @@ let makeReproducibleFilename = (txt) => {
204202
})
205203
};
206204

207-
global.runPrinter = (dirname) => {
205+
global.runPrinter = (dirname, ppx = "") => {
208206
fs.readdirSync(dirname).forEach((base) => {
209207
let filename = path.join(dirname, base);
208+
210209
if (!fs.lstatSync(filename).isFile() || base === "render.spec.js") {
211210
return;
212211
}
213212

214213
test(base, () => {
215-
let {result, errorOutput, status} = printFile(filename);
214+
let {result, errorOutput, status} = printFile(filename, ppx);
216215
if (status > 0) {
217216
let msg = `Test from file: ${filename} failed with error output:
218217
@@ -226,7 +225,9 @@ Make sure the test input is syntactically valid.`;
226225
expect(result).toMatchSnapshot();
227226
}
228227

229-
if (process.env.ROUNDTRIP_TEST) {
228+
// Only run roundtrip tests in ppx-free tests.
229+
// Ppxs are only applied in .res syntax, not .re, so resulting ASTs would not match
230+
if (process.env.ROUNDTRIP_TEST && ppx === "") {
230231
let intf = isInterface(filename);
231232
let sexpAst = parseFileToSexp(filename);
232233
let result2 = parseNapkinStdinToNapkin(result, intf, 80);

0 commit comments

Comments
 (0)