From 1508f5d0a09501a787480b92f99e4927716a3e4b Mon Sep 17 00:00:00 2001 From: David Sancho Date: Mon, 2 Dec 2024 12:08:37 +0100 Subject: [PATCH 1/6] Update README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0025a97ab..37859f5f1 100755 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -> **Warning** -> This repo contains a few parts that are considered experimental. The stable parts are used in production at [app.ahrefs.com](https://app.ahrefs.com) for all users and [wordcount.com](https://wordcount.com), but `Belt`, `Js` modules have missing APIs. non-implemented functions and unsafe code. Use it at your own risk. -> This project enables sharing ReasonReact code between native (compiled to machine code) and Melange (compiled to JavaScript). There are a lot of interesting pieces from this architecture and stack. If you are interested, feel free to contact me in [Discord](https://discord.com/users/122441959414431745) or [Twitter](https://www.twitter.com/davesnx). - # server-reason-react -Re-implementation of `react`, `react-dom` and `react-dom/server` to run on the server and also, a [few related libraries](https://ml-in-barcelona.github.io/server-reason-react/local/server-reason-react/index.html#other-libraries) to enable Server-side Rendering for reason-react applications. +Re-implementation of `react`, `react-dom` and `react-dom/server` to run on the server and also, a [few related libraries](https://ml-in-barcelona.github.io/server-reason-react/local/server-reason-react/index.html#other-libraries) to enable Server-side rendering for reason-react applications, also contains a [few libraries](https://ml-in-barcelona.github.io/server-reason-react/local/server-reason-react/universal-code.html) and a [ppx](https://ml-in-barcelona.github.io/server-reason-react/local/server-reason-react/browser_only.html) to share code between native (compiled to machine code) and JavaScript (compiled by [Melange](https://melange.re)). + +> **Warning** +> This repo contains a few parts that are considered experimental. The stable parts are used in production at [app.ahrefs.com](https://app.ahrefs.com) for all users and [wordcount.com](https://wordcount.com), but `Belt`, `Js` modules have missing APIs, non-implemented functions and unsafe code. Use it at your own risk. ## Why Explained more details in this blog post [sancho.dev/blog/server-side-rendering-react-in-ocaml](https://sancho.dev/blog/server-side-rendering-react-in-ocaml) From b724cc2a608ef03dca2aaaff00bd803d5db3656d Mon Sep 17 00:00:00 2001 From: Pedro Lisboa Date: Mon, 2 Dec 2024 18:01:47 +0000 Subject: [PATCH 2/6] chore: update reason to 3.14 --- demo/universal/native/lib/MelRaw.re | 14 ++++++++------ dune-project | 2 +- server-reason-react.opam | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/demo/universal/native/lib/MelRaw.re b/demo/universal/native/lib/MelRaw.re index b3d556026..8e6843d40 100644 --- a/demo/universal/native/lib/MelRaw.re +++ b/demo/universal/native/lib/MelRaw.re @@ -1,12 +1,13 @@ -let%browser_only mockInitWebsocket = () => - {%mel.raw | +let%browser_only mockInitWebsocket = () => [%mel.raw + {| function mockInitWebsocket() { console.log("Load JS"); } -|}; +|} +]; -let%browser_only initWebsocket = () => - {%mel.raw | +let%browser_only initWebsocket = () => [%mel.raw + {| function initWebsocket() { var socketUrl = "ws://" + location.host + "/_livereload"; var s = new WebSocket(socketUrl); @@ -39,6 +40,7 @@ let%browser_only initWebsocket = () => console.debug("Live reload: WebSocket error:", event); }; } - |}; + |} +]; let x = 22; diff --git a/dune-project b/dune-project index ab83ff0c3..8308dbc06 100644 --- a/dune-project +++ b/dune-project @@ -26,7 +26,7 @@ (depends ; General system dependencies (ocaml (>= 5.0.0)) - (reason (>= 3.11.0)) + (reason (>= 3.14.0)) (melange (>= 3.0.0)) ; Library dependencies diff --git a/server-reason-react.opam b/server-reason-react.opam index 20e94effa..5583f59a8 100644 --- a/server-reason-react.opam +++ b/server-reason-react.opam @@ -9,7 +9,7 @@ bug-reports: "https://github.com/ml-in-barcelona/server-reason-react/issues" depends: [ "dune" {>= "3.9"} "ocaml" {>= "5.0.0"} - "reason" {>= "3.11.0"} + "reason" {>= "3.14.0"} "melange" {>= "3.0.0"} "uucp" {>= "16.0.0"} "ppxlib" {> "0.23.0"} From ea16c107cd41a2e9d01dff358239eec37046951f Mon Sep 17 00:00:00 2001 From: Pedro Lisboa Date: Tue, 3 Dec 2024 13:03:01 +0000 Subject: [PATCH 3/6] fix: format missing mel.raw --- packages/promise/js/promise.re | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/promise/js/promise.re b/packages/promise/js/promise.re index 346bee0e3..f79e645ee 100644 --- a/packages/promise/js/promise.re +++ b/packages/promise/js/promise.re @@ -14,7 +14,8 @@ let onUnhandledException = Js.Console.error(exn); }); -{%%mel.raw | +[%mel.raw + {| function PromiseBox(p) { this.nested = p; }; @@ -71,7 +72,8 @@ function catch_(promise, callback) { return promise.catch(safeCallback); }; -|}; +|} +]; module Js_ = { type t('a, 'e) = rejectable('a, 'e); From abfbf13a868a83d355703e73697d286f4bf8d6cb Mon Sep 17 00:00:00 2001 From: David Sancho Date: Mon, 9 Dec 2024 10:54:55 +0100 Subject: [PATCH 4/6] Move ppx transformation to function body (not last expression) (#192) * Transform pexp_fun into wrapped * Small refactor to explain transformations --- .../cram/component.t/input.re | 22 +++ .../cram/component.t/run.t | 21 ++- .../cram/functor.t/run.t | 6 +- .../server_reason_react_ppx.ml | 109 +++++------- packages/server-reason-react-ppx/test/test.re | 165 +++++++++++------- 5 files changed, 195 insertions(+), 128 deletions(-) diff --git a/packages/server-reason-react-ppx/cram/component.t/input.re b/packages/server-reason-react-ppx/cram/component.t/input.re index 46efc50b5..30c0674a0 100644 --- a/packages/server-reason-react-ppx/cram/component.t/input.re +++ b/packages/server-reason-react-ppx/cram/component.t/input.re @@ -81,3 +81,25 @@ module Async_component = { }; let a =
; + +module Sequence = { + [@react.component] + let make = (~lola) => { + let (state, setState) = React.useState(lola); + + React.useEffect(() => { + setState(lola); + None; + }); + +
{React.string(state)}
; + }; +}; + +module Use_context = { + [@react.component] + let make = () => { + let captured = React.useContext(Context.value); +
{React.string(captured)}
; + }; +}; diff --git a/packages/server-reason-react-ppx/cram/component.t/run.t b/packages/server-reason-react-ppx/cram/component.t/run.t index 0377b8d81..0f5d254ec 100644 --- a/packages/server-reason-react-ppx/cram/component.t/run.t +++ b/packages/server-reason-react-ppx/cram/component.t/run.t @@ -27,9 +27,9 @@ We need to output ML syntax here, otherwise refmt could not parse it. module Onclick_handler_button = struct let make ?key:(_ : string option) ~name ?isDisabled () = - let onClick event = Js.log event in React.Upper_case_component (fun () -> + let onClick event = Js.log event in React.createElement "button" (Stdlib.List.filter_map Fun.id [ @@ -123,3 +123,22 @@ We need to output ML syntax here, otherwise refmt could not parse it. end let a = Async_component.make ~children:(React.createElement "div" [] []) () + + module Sequence = struct + let make ?key:(_ : string option) ~lola () = + React.Upper_case_component + (fun () -> + let state, setState = React.useState lola in + React.useEffect (fun () -> + setState lola; + None); + React.createElement "div" [] [ React.string state ]) + end + + module Use_context = struct + let make ?key:(_ : string option) () = + React.Upper_case_component + (fun () -> + let captured = React.useContext Context.value in + React.createElement "div" [] [ React.string captured ]) + end diff --git a/packages/server-reason-react-ppx/cram/functor.t/run.t b/packages/server-reason-react-ppx/cram/functor.t/run.t index cdb0ef7ee..5df02d7be 100644 --- a/packages/server-reason-react-ppx/cram/functor.t/run.t +++ b/packages/server-reason-react-ppx/cram/functor.t/run.t @@ -9,6 +9,8 @@ We need to output ML syntax here, otherwise refmt could not parse it. let x = M.x + 1 let make ?key:(_ : string option) ~a ~b () = - print_endline "This function should be named `Test$Func`" M.x; - React.Upper_case_component (fun () -> React.createElement "div" [] []) + React.Upper_case_component + (fun () -> + print_endline "This function should be named `Test$Func`" M.x; + React.createElement "div" [] []) end diff --git a/packages/server-reason-react-ppx/server_reason_react_ppx.ml b/packages/server-reason-react-ppx/server_reason_react_ppx.ml index e766af6b5..7ef925fbb 100644 --- a/packages/server-reason-react-ppx/server_reason_react_ppx.ml +++ b/packages/server-reason-react-ppx/server_reason_react_ppx.ml @@ -14,17 +14,13 @@ let pexp_list ~loc xs = exception Error of expression let raise_errorf ~loc fmt = - let open Ast_builder.Default in Printf.ksprintf (fun msg -> let expr = pexp_extension ~loc (Location.error_extensionf ~loc "%s" msg) in raise (Error expr)) fmt -let make_string ~loc str = - let open Ast_helper in - Ast_helper.Exp.constant ~loc (Const.string str) - +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" @@ -400,70 +396,55 @@ let get_function_name binding = | { pvb_pat = { ppat_desc = Ppat_var { txt } } } -> txt | _ -> raise_errorf ~loc:binding.pvb_loc "react.component calls cannot be destructured." -(* TODO: there is a long-tail of unsupported features inside of blocks - Pexp_letmodule , Pexp_letexception , Pexp_ifthenelse *) -let rec transform_function_with_warning expression = +(* TODO: there are a few unsupported features inside of blocks - Pexp_letmodule , Pexp_letexception , Pexp_ifthenelse *) +let add_unit_at_the_last_argument expression = let loc = expression.pexp_loc in - match expression.pexp_desc with - (* let make = (~prop) => ... with no final unit *) - | Pexp_fun (((Labelled _ | Optional _) as label), default, pattern, ({ pexp_desc = Pexp_fun _ } as internalExpression)) - -> - let exp = transform_function_with_warning internalExpression in - { expression with pexp_desc = Pexp_fun (label, default, pattern, exp) } - (* let make = (()) => ... *) - (* let make = (_) => ... *) - | Pexp_fun - (Nolabel, _default, { ppat_desc = Ppat_construct ({ txt = Lident "()" }, _) | Ppat_any }, _internalExpression) -> - expression - (* let make = (~prop) => ... *) - | 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_loc = expression.pexp_loc; - pexp_desc = Pexp_fun (Nolabel, None, [%pat? ()], internalExpression); - pexp_loc_stack = []; - pexp_attributes = []; - } ); - } - (* let make = {let foo = bar in (~prop) => ...} *) - | Pexp_let (recursive, vbs, internalExpression) -> - (* here's where we spelunk! *) - let exp = transform_function_with_warning internalExpression in - { expression with pexp_desc = Pexp_let (recursive, vbs, exp) } - (* let make = React.forwardRef((~prop) => ...) *) - | Pexp_apply (_wrapperExpression, [ (Nolabel, internalExpression) ]) -> - transform_function_with_warning internalExpression - (* let make = React.memoCustomCompareProps((~prop) => ..., (prevPros, nextProps) => true) *) - | Pexp_apply - (_wrapperExpression, [ (Nolabel, internalExpression); ((Nolabel, { pexp_desc = Pexp_fun _ }) as _compareProps) ]) - -> - transform_function_with_warning internalExpression - | Pexp_sequence (wrapperExpression, internalExpression) -> - let exp = transform_function_with_warning internalExpression in - { expression with pexp_desc = Pexp_sequence (wrapperExpression, exp) } - | _ -> expression - -let transofrm_last_expression expr fn = + let rec inner expression = + match expression.pexp_desc with + (* let make = (~prop) => ... with no final unit *) + | 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) + (* let make = (()) => ... *) + (* let make = (_) => ... *) + | Pexp_fun (Nolabel, _, { ppat_desc = Ppat_construct ({ txt = Lident "()" }, _) | Ppat_any }, _) -> expression + (* let make = (~prop) => ... *) + | 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); + } + (* let make = {let foo = bar in (~prop) => ...} *) + | Pexp_let (recursive, vbs, internalExpression) -> + pexp_let ~loc:expression.pexp_loc recursive vbs (inner internalExpression) + (* let make = React.forwardRef((~prop) => ...) *) + | Pexp_apply (_, [ (Nolabel, internalExpression) ]) -> inner internalExpression + (* let make = React.memoCustomCompareProps((~prop) => ..., (prevPros, nextProps) => true) *) + | 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_sequence (expr, sequence) -> pexp_sequence ~loc:expr.pexp_loc expr (inner sequence) - | Pexp_let (flag, patt, expression) -> pexp_let ~loc:expr.pexp_loc flag patt (inner expression) | Pexp_fun (label, def, patt, expression) -> pexp_fun ~loc:expr.pexp_loc label def patt (inner expression) | _ -> fn expr in inner expr -let make_value_binding binding wrapping = +let make_value_binding binding react_element_variant_wrapping = let loc = binding.pvb_loc in let ghost_loc = { binding.pvb_loc with loc_ghost = true } in - let binding_expr = transform_function_with_warning binding.pvb_expr 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 (* Builds an AST node for the modified `make` function *) let name = Ast_helper.Pat.mk ~loc:ghost_loc (Ppat_var { txt = get_function_name binding; loc = ghost_loc }) in let key_arg = Optional "key" in @@ -474,10 +455,8 @@ let make_value_binding binding wrapping = let key_pattern = ppat_constraint ~loc key_renamed_to_underscore core_type in (* Append key argument since we want to allow users of this component to set key (and assign it to _ since it shouldn't be used) *) - let body_expression = - pexp_fun ~loc:ghost_loc key_arg default_value key_pattern (transofrm_last_expression binding_expr wrapping) - in - Ast_helper.Vb.mk ~loc name body_expression + let function_body = pexp_fun ~loc:ghost_loc key_arg default_value key_pattern binding_expr in + Ast_helper.Vb.mk ~loc name function_body let rewrite_signature_item signature_item = (* Remove the [@react.component] from the AST *) @@ -498,9 +477,9 @@ let rewrite_signature_item signature_item = let loc = signature_item.psig_loc in [%sigi: [%%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"]]) + "externals aren't supported on server-reason-react. externals are used to bind to React components from \ + JavaScript. In the server, that doesn't make sense. If you need to render this on the server, implement a \ + stub component or an empty element (React.null)"]]) | _signature_item -> signature_item let rewrite_structure_item structure_item = diff --git a/packages/server-reason-react-ppx/test/test.re b/packages/server-reason-react-ppx/test/test.re index f99d2972f..221a29bc0 100644 --- a/packages/server-reason-react-ppx/test/test.re +++ b/packages/server-reason-react-ppx/test/test.re @@ -1,25 +1,25 @@ -let test = (title, fn) => Alcotest.test_case(title, `Quick, fn); +let test = (title, fn) => (title, [Alcotest.test_case("", `Quick, fn)]); let assert_string = (left, right) => { Alcotest.check(Alcotest.string, "should be equal", right, left); }; let tag = () => { - let div =
; - assert_string(ReactDOM.renderToStaticMarkup(div), {|
|}); + assert_string(ReactDOM.renderToStaticMarkup(
), {|
|}); }; let empty_attribute = () => { - let div =
; assert_string( - ReactDOM.renderToStaticMarkup(div), + ReactDOM.renderToStaticMarkup(
), {|
|}, ); }; let bool_attribute = () => { - let div =