-
Notifications
You must be signed in to change notification settings - Fork 38
Move React ppx from compiler repo, add tests #124
Changes from 8 commits
523ff1e
d24c0df
a3f0b44
8a7a1bd
d3dcd85
dd2f680
e84640a
739a1df
23336db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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 |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -165,6 +165,7 @@ module ResClflags: sig | |||||||||
| val files: string list ref | ||||||||||
| val interface: bool ref | ||||||||||
| val report: string ref | ||||||||||
| val ppx: string ref | ||||||||||
|
|
||||||||||
| val parse: unit -> unit | ||||||||||
| end = struct | ||||||||||
|
|
@@ -178,6 +179,7 @@ end = struct | |||||||||
| let origin = ref "" | ||||||||||
| let interface = ref false | ||||||||||
| let report = ref "pretty" | ||||||||||
| let ppx = ref "none" | ||||||||||
|
||||||||||
|
|
||||||||||
| let usage = "Usage:\n rescript <options> <file>\n\n" ^ | ||||||||||
| "Examples:\n" ^ | ||||||||||
|
|
@@ -192,6 +194,7 @@ end = struct | |||||||||
| ("-print", Arg.String (fun txt -> print := txt), "Print either binary or ns. Default: ns"); | ||||||||||
| ("-width", Arg.Int (fun w -> width := w), "Specify the line length for the printer (formatter)"); | ||||||||||
| ("-interface", Arg.Unit (fun () -> interface := true), "Parse as interface"); | ||||||||||
| ("-ppx", Arg.String (fun txt -> ppx := txt), "Apply a specific built-in ppx before parsing, none or jsx. Default: none"); | ||||||||||
| (* ("-report", Arg.String (fun txt -> report := txt), "Stylize errors and messages using color and context. Accepts `Pretty` and `Plain`. Default `Plain`") *) | ||||||||||
| ] | ||||||||||
|
|
||||||||||
|
|
@@ -201,7 +204,7 @@ end | |||||||||
| module CliArgProcessor = struct | ||||||||||
| type backend = Parser: ('diagnostics) Res_driver.parsingEngine -> backend [@@unboxed] | ||||||||||
|
|
||||||||||
| let processFile ~isInterface ~width ~recover ~origin ~target ~report:_ filename = | ||||||||||
| let processFile ~isInterface ~width ~recover ~origin ~target ~report:_ ~ppx filename = | ||||||||||
| try | ||||||||||
| let len = String.length filename in | ||||||||||
| let processInterface = | ||||||||||
|
|
@@ -227,6 +230,11 @@ module CliArgProcessor = struct | |||||||||
| | _ -> false | ||||||||||
| in | ||||||||||
|
|
||||||||||
| let ppx = match ppx with | ||||||||||
|
||||||||||
| | "jsx" -> `Jsx | ||||||||||
| | _ -> `None | ||||||||||
| in | ||||||||||
|
|
||||||||||
| let Parser backend = parsingEngine in | ||||||||||
| (* This is the whole purpose of the Color module above *) | ||||||||||
| Color.setup None; | ||||||||||
|
|
@@ -243,8 +251,12 @@ module CliArgProcessor = struct | |||||||||
| else exit 1 | ||||||||||
| end | ||||||||||
| else | ||||||||||
| let parsetree = match ppx with | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we keep this a string and match on the string for now? Does this extra conversion give us something in the end? |
||||||||||
| | `Jsx -> Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree | ||||||||||
| | `None -> parseResult.parsetree | ||||||||||
| in | ||||||||||
| printEngine.printInterface | ||||||||||
| ~width ~filename ~comments:parseResult.comments parseResult.parsetree | ||||||||||
| ~width ~filename ~comments:parseResult.comments parsetree | ||||||||||
| else | ||||||||||
| let parseResult = backend.parseImplementation ~forPrinter ~filename in | ||||||||||
| if parseResult.invalid then begin | ||||||||||
|
|
@@ -258,19 +270,23 @@ module CliArgProcessor = struct | |||||||||
| else exit 1 | ||||||||||
| end | ||||||||||
| else | ||||||||||
| let parsetree = match ppx with | ||||||||||
| | `Jsx -> Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree | ||||||||||
| | `None -> parseResult.parsetree | ||||||||||
| in | ||||||||||
| printEngine.printImplementation | ||||||||||
| ~width ~filename ~comments:parseResult.comments parseResult.parsetree | ||||||||||
| ~width ~filename ~comments:parseResult.comments parsetree | ||||||||||
| with | ||||||||||
| | Failure txt -> | ||||||||||
| prerr_string txt; | ||||||||||
| prerr_newline(); | ||||||||||
| exit 1 | ||||||||||
| | _ -> exit 1 | ||||||||||
| [@@raises exit] | ||||||||||
| [@@raises Invalid_argument, exit] | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What raises
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The existing react ppx uses that exception to note cases with unexpected situations. I can count at least 19 different places, e.g. Lines 351 to 354 in 23336db
|
||||||||||
| end | ||||||||||
|
|
||||||||||
|
|
||||||||||
| let [@raises exit] () = | ||||||||||
| let [@raises Invalid_argument, exit] () = | ||||||||||
| if not !Sys.interactive then begin | ||||||||||
| ResClflags.parse (); | ||||||||||
| match !ResClflags.files with | ||||||||||
|
|
@@ -282,6 +298,7 @@ let [@raises exit] () = | |||||||||
| ~target:!ResClflags.print | ||||||||||
| ~origin:!ResClflags.origin | ||||||||||
| ~report:!ResClflags.report | ||||||||||
| ~ppx:!ResClflags.ppx | ||||||||||
| "" | ||||||||||
| | files -> | ||||||||||
| List.iter (fun filename -> | ||||||||||
|
|
@@ -292,6 +309,7 @@ let [@raises exit] () = | |||||||||
| ~target:!ResClflags.print | ||||||||||
| ~origin:!ResClflags.origin | ||||||||||
| ~report:!ResClflags.report | ||||||||||
| ~ppx:!ResClflags.ppx | ||||||||||
| filename | ||||||||||
| ) files | ||||||||||
| end | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // test React JSX file | ||
|
|
||
| @react.component | ||
| let make = (~msg) => { | ||
| <div> {msg->React.string} </div> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should make a separate folder for ppx tests: |
||
|
|
||
| exports[`commentAtTop.res 1`] = ` | ||
| "@bs.obj | ||
| external makeProps: (~msg: 'msg, ~key: string=?, unit) => {\\"msg\\": 'msg} = \\"\\" // test React JSX file | ||
| let make = | ||
| (@warning(\\"-16\\") ~msg) => { | ||
| ReactDOMRe.createDOMElementVariadic(\\"div\\", [{msg->React.string}]) | ||
| } | ||
| let make = { | ||
| let \\\\\\"CommentAtTop\\" = (\\\\\\"Props\\": {\\"msg\\": 'msg}) => make(~msg=\\\\\\"Props\\"[\\"msg\\"]) | ||
| \\\\\\"CommentAtTop\\" | ||
| } | ||
| " | ||
| `; | ||
| exports[`externalWithCustomName.res 1`] = ` | ||
| "module Foo = { | ||
| @bs.obj | ||
| external componentProps: ( | ||
| ~a: int, | ||
| ~b: string, | ||
| ~key: string=?, | ||
| unit, | ||
| ) => {\\"a\\": int, \\"b\\": string} = \\"\\" | ||
| @bs.module(\\"Foo\\") | ||
| external component: React.componentLike< | ||
| {\\"a\\": int, \\"b\\": string}, | ||
| React.element, | ||
| > = \\"component\\" | ||
| } | ||
| let t = React.createElement( | ||
| Foo.component, | ||
| Foo.componentProps(~a=1, ~b={\\"1\\"}, ()), | ||
| ) | ||
| " | ||
| `; | ||
| exports[`innerModule.res 1`] = ` | ||
| "module Bar = { | ||
| @bs.obj | ||
| external makeProps: ( | ||
| ~a: 'a, | ||
| ~b: 'b, | ||
| ~key: string=?, | ||
| 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\\", []) | ||
| } | ||
| let make = { | ||
| let \\\\\\"InnerModule$Bar\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => | ||
| make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) | ||
| \\\\\\"InnerModule$Bar\\" | ||
| } | ||
| @bs.obj | ||
| external componentProps: ( | ||
| ~a: 'a, | ||
| ~b: 'b, | ||
| ~key: string=?, | ||
| 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\\", []) | ||
| } | ||
| let component = { | ||
| let \\\\\\"InnerModule$Bar$component\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => | ||
| component(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) | ||
| \\\\\\"InnerModule$Bar$component\\" | ||
| } | ||
| } | ||
| " | ||
| `; | ||
| exports[`topLevel.res 1`] = ` | ||
| "@bs.obj | ||
| external makeProps: ( | ||
| ~a: 'a, | ||
| ~b: 'b, | ||
| ~key: string=?, | ||
| 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\\", []) | ||
| } | ||
| let make = { | ||
| let \\\\\\"TopLevel\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => | ||
| make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) | ||
| \\\\\\"TopLevel\\" | ||
| } | ||
| " | ||
| `; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // test React JSX file | ||
|
|
||
| @react.component | ||
| let make = (~msg) => { | ||
| <div> {msg->React.string} </div> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| module Foo = { | ||
| @react.component @bs.module("Foo") | ||
| external component: (~a: int, ~b: string, _) => React.element = "component" | ||
| } | ||
|
|
||
| let t = <Foo.component a=1 b={"1"} /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| module Bar = { | ||
| @react.component | ||
| let make = (~a, ~b, _) => { | ||
| Js.log( | ||
| "This function should be named `InnerModule.react$Bar`", | ||
| ) | ||
| <div /> | ||
| } | ||
| @react.component | ||
| let component = (~a, ~b, _) => { | ||
| Js.log( | ||
| "This function should be named `InnerModule.react$Bar$component`", | ||
| ) | ||
| <div /> | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| runPrinter(__dirname, "jsx") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| @react.component | ||
| let make = (~a, ~b, _) => { | ||
| Js.log("This function should be named 'TopLevel.react'") | ||
| <div /> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -151,7 +151,7 @@ function parseNapkinStdinToNapkin(src, isInterface, width = 100) { | |
| .stdout.toString("utf8"); | ||
| } | ||
|
|
||
| function printFile(filename) { | ||
| function printFile(filename, ppx) { | ||
| let parserSrc; | ||
| switch (classifyLang(filename)) { | ||
| case "ocaml": | ||
|
|
@@ -165,17 +165,15 @@ function printFile(filename) { | |
| status: 0, | ||
| errorOutput: "" | ||
| }; | ||
| break; | ||
|
|
||
|
|
||
| case "rescript": | ||
| default: | ||
| parserSrc = "res"; | ||
| break; | ||
| } | ||
|
|
||
| let intf = isInterface(filename); | ||
|
|
||
| let args = ["-parse", parserSrc, "-print", "res", "-width", "80"]; | ||
| let args = ["-parse", parserSrc, "-print", "res", "-width", "80", "-ppx", ppx]; | ||
|
|
||
| if (intf) { | ||
| args.push("-interface"); | ||
|
|
@@ -204,15 +202,16 @@ let makeReproducibleFilename = (txt) => { | |
| }) | ||
| }; | ||
|
|
||
| global.runPrinter = (dirname) => { | ||
| global.runPrinter = (dirname, ppx = "none") => { | ||
|
||
| fs.readdirSync(dirname).forEach((base) => { | ||
| let filename = path.join(dirname, base); | ||
|
|
||
| if (!fs.lstatSync(filename).isFile() || base === "render.spec.js") { | ||
| return; | ||
| } | ||
|
|
||
| test(base, () => { | ||
| let {result, errorOutput, status} = printFile(filename); | ||
| let {result, errorOutput, status} = printFile(filename, ppx); | ||
| if (status > 0) { | ||
| let msg = `Test from file: ${filename} failed with error output: | ||
|
|
||
|
|
@@ -226,7 +225,7 @@ Make sure the test input is syntactically valid.`; | |
| expect(result).toMatchSnapshot(); | ||
| } | ||
|
|
||
| if (process.env.ROUNDTRIP_TEST) { | ||
| if (process.env.ROUNDTRIP_TEST && ppx === "none") { | ||
|
||
| let intf = isInterface(filename); | ||
| let sexpAst = parseFileToSexp(filename); | ||
| let result2 = parseNapkinStdinToNapkin(result, intf, 80); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does the ppx need a separate
build-ppxrule? If it's a part "API_FILES", is this sufficient?