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

WIP: React JSX PPX v4 #235

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .depend
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
src/reactjs_jsx_ppx_v3.cmx : src/reactjs_jsx_ppx_v3.cmi
src/reactjs_jsx_ppx_v3.cmi :
src/reactjs_jsx_ppx_v4.cmx : src/reactjs_jsx_ppx_v4.cmi
src/reactjs_jsx_ppx_v4.cmi :
src/res_ast_conversion.cmx : src/res_ast_conversion.cmi
src/res_ast_conversion.cmi :
src/res_ast_debugger.cmx : src/res_driver.cmx src/res_doc.cmx \
Expand All @@ -8,7 +10,8 @@ src/res_ast_debugger.cmi : src/res_driver.cmi
src/res_character_codes.cmx :
src/res_cli.cmx : src/res_driver_reason_binary.cmx \
src/res_driver_ml_parser.cmx src/res_driver_binary.cmx src/res_driver.cmx \
src/res_ast_debugger.cmx src/reactjs_jsx_ppx_v3.cmx
src/res_ast_debugger.cmx src/reactjs_jsx_ppx_v4.cmx \
src/reactjs_jsx_ppx_v3.cmx
src/res_comment.cmx : src/res_comment.cmi
src/res_comment.cmi :
src/res_comments_table.cmx : src/res_parsetree_viewer.cmx src/res_doc.cmx \
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ depend:

API_FILES = \
src/reactjs_jsx_ppx_v3.cmx\
src/reactjs_jsx_ppx_v4.cmx\
src/res_io.cmx\
src/res_minibuffer.cmx\
src/res_doc.cmx\
Expand Down
1,455 changes: 1,455 additions & 0 deletions src/reactjs_jsx_ppx_v4.ml

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions src/reactjs_jsx_ppx_v4.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
(*
This is the module that handles turning Reason JSX' agnostic function call into
a ReasonReact-specific function call. Aka, this is a macro, using OCaml's ppx
facilities; https://whitequark.org/blog/2014/04/16/a-guide-to-extension-
points-in-ocaml/
You wouldn't use this file directly; it's used by ReScript's
bsconfig.json. Specifically, there's a field called `react-jsx` inside the
field `reason`, which enables this ppx through some internal call in bsb
*)

(*
There are two different transforms that can be selected in this file (v2 and v3):
v2:
transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into
`ReactDOMRe.createElement("div", ~props={"props1": 1, "props2": b}, [|foo,
bar|])`.
transform `[@JSX] div(~props1=a, ~props2=b, ~children=foo, ())` into
`ReactDOMRe.createElementVariadic("div", ~props={"props1": 1, "props2": b}, foo)`.
transform the upper-cased case
`[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into
`ReasonReact.element(~key=a, ~ref=b, Foo.make(~foo=bar, [||]))`
transform `[@JSX] [foo]` into
`ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
v3:
transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into
`ReactDOMRe.createDOMElementVariadic("div", ReactDOMRe.domProps(~props1=1, ~props2=b), [|foo, bar|])`.
transform the upper-cased case
`[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into
`React.createElement(Foo.make, Foo.makeProps(~key=a, ~ref=b, ~foo=bar, ()))`
transform the upper-cased case
`[@JSX] Foo.createElement(~foo=bar, ~children=[foo, bar], ())` into
`React.createElementVariadic(Foo.make, Foo.makeProps(~foo=bar, ~children=React.null, ()), [|foo, bar|])`
transform `[@JSX] [foo]` into
`ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])`
*)

val rewrite_implementation : Parsetree.structure -> Parsetree.structure

val rewrite_signature : Parsetree.signature -> Parsetree.signature
9 changes: 7 additions & 2 deletions src/res_cli.ml
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ module CliArgProcessor = struct
end
else
let parsetree = match ppx with
| "jsx" -> Reactjs_jsx_ppx_v3.rewrite_signature parseResult.parsetree
| "jsx" -> Reactjs_jsx_ppx_v4.rewrite_signature parseResult.parsetree
| "jsx3" -> Reactjs_jsx_ppx_v3.rewrite_signature parseResult.parsetree
| _ -> parseResult.parsetree
in
printEngine.printInterface
Expand All @@ -266,7 +267,8 @@ module CliArgProcessor = struct
end
else
let parsetree = match ppx with
| "jsx" -> Reactjs_jsx_ppx_v3.rewrite_implementation parseResult.parsetree
| "jsx" -> Reactjs_jsx_ppx_v4.rewrite_implementation parseResult.parsetree
| "jsx3" -> Reactjs_jsx_ppx_v3.rewrite_implementation parseResult.parsetree
| _ -> parseResult.parsetree
in
printEngine.printImplementation
Expand All @@ -276,6 +278,9 @@ module CliArgProcessor = struct
prerr_string txt;
prerr_newline();
exit 1
| Location.Error _ as err ->
Location.report_exception Format.err_formatter err;
exit 1
| _ -> exit 1
[@@raises Invalid_argument, exit]
end
Expand Down
113 changes: 85 additions & 28 deletions tests/ppx/react/__snapshots__/render.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`aria.res 1`] = `
"let foo = (@warning(\\"-3\\") React.jsx)(
ReactDOM.stringToComponent(\\"div\\"),
{
let ariaCurrent = #page
{
\\"aria-current\\": ariaCurrent: [
| #page
| #step
| #location
| #date
| #time
| #\\"true\\"
| #\\"false\\"
],
}
},
)
"
`;

exports[`commentAtTop.res 1`] = `
"@obj
external makeProps: (~msg: 'msg, ~key: string=?, unit) => {\\"msg\\": 'msg} = \\"\\" // test React JSX file
"@obj external makeProps: (~msg: 'msg, unit) => {\\"msg\\": 'msg} = \\"\\" // test React JSX file

let make =
(@warning(\\"-16\\") ~msg) => {
ReactDOMRe.createDOMElementVariadic(\\"div\\", [{msg->React.string}])
(@warning(\\"-3\\") React.jsx)(
{
ReactDOM.stringToComponent(\\"div\\")
},
{
let children = {msg->React.string}
{\\"children\\": children: React.element}
},
)
}
let make = {
let \\\\\\"CommentAtTop\\" = (\\\\\\"Props\\": {\\"msg\\": 'msg}) => make(~msg=\\\\\\"Props\\"[\\"msg\\"])
Expand All @@ -15,13 +43,43 @@ let make = {
"
`;

exports[`data.res 1`] = `
"let foo = (@warning(\\"-3\\") React.jsx)(
ReactDOM.stringToComponent(\\"div\\"),
{
let \\\\\\"data-foo\\" = \\"payload\\"
{\\"data-foo\\": \\\\\\"data-foo\\": string}
},
)
"
`;

exports[`error_badComponent.res 1`] = `
"=====Result==========================================

=====Errors=============================================
File \\"Error_badComponent\\", line 1:
Error: React: react.component calls can only decorate function definitions or component wrappers (forwardRef, memo).

========================================================"
`;

exports[`error_tooManyReactComponent.res 1`] = `
"=====Result==========================================

=====Errors=============================================
File \\"/Users/rickyvetter/code/syntax/tests/ppx/react/error_tooManyReactComponent.res\\", line 1, characters 0-105:
Error: React: only one react.component call can exist on a component at one time

========================================================"
`;

exports[`externalWithCustomName.res 1`] = `
"module Foo = {
@obj
external componentProps: (
~a: int,
~b: string,
~key: string=?,
unit,
) => {\\"a\\": int, \\"b\\": string} = \\"\\"
@module(\\"Foo\\")
Expand All @@ -31,44 +89,49 @@ exports[`externalWithCustomName.res 1`] = `
> = \\"component\\"
}

let t = React.createElement(
let t = (@warning(\\"-3\\") React.jsx)(
Foo.component,
Foo.componentProps(~a=1, ~b={\\"1\\"}, ()),
)
"
`;

exports[`fragment.res 1`] = `
"let foo =
@warning(\\"-3\\")
React.jsxs(
React.Fragment.make,
React.Fragment.makeProps(
~children=React.array([
(@warning(\\"-3\\") React.jsx)(ReactDOM.stringToComponent(\\"div\\"), ()),
(@warning(\\"-3\\") React.jsx)(ReactDOM.stringToComponent(\\"span\\"), ()),
]),
(),
),
)
"
`;

exports[`innerModule.res 1`] = `
"module Bar = {
@obj
external makeProps: (
~a: 'a,
~b: 'b,
~key: string=?,
unit,
) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
@obj external makeProps: (~a: 'a, ~b: 'b, unit) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
let make =
(@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => {
Js.log(\\"This function should be named \`InnerModule.react$Bar\`\\")
ReactDOMRe.createDOMElementVariadic(\\"div\\", [])
(@warning(\\"-3\\") React.jsx)(ReactDOM.stringToComponent(\\"div\\"), ())
}
let make = {
let \\\\\\"InnerModule$Bar\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) =>
make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ())
\\\\\\"InnerModule$Bar\\"
}
@obj
external componentProps: (
~a: 'a,
~b: 'b,
~key: string=?,
unit,
) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
external componentProps: (~a: 'a, ~b: 'b, unit) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"

let component =
(@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => {
Js.log(\\"This function should be named \`InnerModule.react$Bar$component\`\\")
ReactDOMRe.createDOMElementVariadic(\\"div\\", [])
(@warning(\\"-3\\") React.jsx)(ReactDOM.stringToComponent(\\"div\\"), ())
}
let component = {
let \\\\\\"InnerModule$Bar$component\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) =>
Expand All @@ -80,17 +143,11 @@ exports[`innerModule.res 1`] = `
`;

exports[`topLevel.res 1`] = `
"@obj
external makeProps: (
~a: 'a,
~b: 'b,
~key: string=?,
unit,
) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
"@obj external makeProps: (~a: 'a, ~b: 'b, unit) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\"
let make =
(@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => {
Js.log(\\"This function should be named 'TopLevel.react'\\")
ReactDOMRe.createDOMElementVariadic(\\"div\\", [])
(@warning(\\"-3\\") React.jsx)(ReactDOM.stringToComponent(\\"div\\"), ())
}
let make = {
let \\\\\\"TopLevel\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) =>
Expand Down
1 change: 1 addition & 0 deletions tests/ppx/react/aria.res
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let foo = <div ariaCurrent=#page />
1 change: 1 addition & 0 deletions tests/ppx/react/data.res
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let foo = <div \"data-foo"="payload" />
2 changes: 2 additions & 0 deletions tests/ppx/react/error_badComponent.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@react.component
let foo = 1
4 changes: 4 additions & 0 deletions tests/ppx/react/error_tooManyReactComponent.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@react.component
@react.component
@bs.module("file")
external make: (~prop: string) => React.element = ""
1 change: 1 addition & 0 deletions tests/ppx/react/fragment.res
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let foo = <><div/><span/></>
2 changes: 1 addition & 1 deletion tests/ppx/react/render.spec.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
runPrinter(__dirname, "jsx")
runPrinter(__dirname, "jsx", true)
19 changes: 15 additions & 4 deletions tests/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ let makeReproducibleFilename = (txt) => {
})
};

global.runPrinter = (dirname, ppx = "") => {
global.runPrinter = (dirname, ppx = "", showError = false) => {
fs.readdirSync(dirname).forEach((base) => {
let filename = path.join(dirname, base);

Expand All @@ -212,7 +212,7 @@ global.runPrinter = (dirname, ppx = "") => {

test(base, () => {
let {result, errorOutput, status} = printFile(filename, ppx);
if (status > 0) {
if (!showError && status > 0) {
let msg = `Test from file: ${filename} failed with error output:

------------ BEGIN ------------
Expand All @@ -222,7 +222,18 @@ ${errorOutput}
Make sure the test input is syntactically valid.`;
fail(msg);
} else {
expect(result).toMatchSnapshot();
let output = "";
if (showError && status > 0) {
output += `=====Result==========================================\n`;
output += `${result}\n`;
output += `=====Errors=============================================\n`;
output += `${makeReproducibleFilename(errorOutput.toString())}\n`;
output += `========================================================`;
} else {
output = result;
}

expect(output).toMatchSnapshot();
}

// Only run roundtrip tests in ppx-free tests.
Expand All @@ -239,7 +250,7 @@ Make sure the test input is syntactically valid.`;
});
};

global.runParser = (dirname, recover = false, showError = false, env) => {
global.runParser = (dirname, recover = false, showError = false, env, ppx = "") => {
fs.readdirSync(dirname).forEach((base) => {
let filename = path.join(dirname, base);
if (!fs.lstatSync(filename).isFile() || base === "parse.spec.js") {
Expand Down