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)
diff --git a/arch/server/package.json b/arch/server/package.json
index ce9622f1b..f111f7a82 100644
--- a/arch/server/package.json
+++ b/arch/server/package.json
@@ -3,7 +3,8 @@
"version": "0.0.1",
"scripts": {
"react-dom-server": "node react-dom-server.js",
- "render-to-stream": "bun render-to-stream.js",
+ "render-html-to-stream": "bun render-html-to-stream.js",
+ "render-rsc-to-stream": "bun --conditions react-server render-rsc-to-stream.js",
"react-server-dom-webpack": "node --conditions react-server react-server-dom-webpack.js"
},
"license": "MIT",
diff --git a/arch/server/react-server-dom-webpack.js b/arch/server/react-server-dom-webpack.js
deleted file mode 100644
index efdcf186e..000000000
--- a/arch/server/react-server-dom-webpack.js
+++ /dev/null
@@ -1,106 +0,0 @@
-const React = require("react");
-const ReactServerDOM = require("react-server-dom-webpack/server");
-
-let sleep = (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));
-
-
-let Text = ({children}) => React.createElement("span", {}, children);
-/* let App = () => {
- return React.createElement("div", null, [
- React.createElement("span", {key: "home"}, ["Home"]),
- React.createElement("span", {key: "nohome"}, ["Nohome"]),
- ]);
-}; */
-
-/* let Text = async ({children}) => {
- let value = await sleep(2000, "lola");
- return value;
-};
-
-let ClientComponent = {
- $$typeof: Symbol.for("react.client.reference"),
- $$id: "ClientComponent",
- $$async: false,
- default: () => "Hello from the client!",
- name: "ClientComponent",
-};
-
-let App = () => {
- return React.createElement(React.Suspense, {fallback: "Loading..."}, [
- React.createElement(Text, {key: "hi"}, "hi"),
- React.createElement(ClientComponent.default, {key: "cc"}, []),
- ]);
-};
-
-let main = React.createElement(App, {}, []);
- */
-
-/* let App = () => {
- return React.createElement("div", {dangerouslySetInnerHTML: {__html: "console.log(\"hi\")"}}, []);
-}; */
-
- /* let app codition =
- React.Upper_case_component
- (fun () ->
- let text = if codition then "foo" else "bar" in
- React.createElement "span" [] [ React.string text ])
- in */
-
-/* let Foo = () => {
- return React.createElement("span", {}, "foo");
-};
-
-let App = () => {
- return React.createElement(Foo, {}, []);
-}; */
-
-/* let Text = ({children}) => React.createElement("span", {}, children);
-
-let App = () => ([
- React.createElement(Text, {}, "hi"),
- React.createElement(Text, {}, "hola"),
-]); */
-
-// 2:["$","span",null,{"children":"hi"},"$3"]
-// 4:["$","span",null,{"children":"hola"},"$5"]
-// 0:["$2","$4"]
-
-/* let Layout = ({children}) => React.createElement("div", {}, children);
-
-
-let App = () =>
- React.createElement(
- Layout,
- {},
- [
- React.createElement(Text, {key: "hi"}, "hi"),
- React.createElement(Text, {key: "hola"}, "hola"),
- ],
- ); */
-
- /* let app () =
- React.Suspense.make
- ~fallback:(React.string "Loading...")
- ~children:
- (React.createElement "div" []
- [
- React.Upper_case_component (text ~children:[ React.string "hi" ]);
- React.Upper_case_component (text ~children:[ React.string "hola" ]);
- ])
- ()
- in
- let main = React.Upper_case_component app in
- let%lwt stream = ReactServerDOM.render_to_model main in */
-
-let App = () => {
- return React.createElement(React.Suspense, {fallback: "Loading..."}, [
- React.createElement("div", null, [
- React.createElement(Text, null, "hi"),
- React.createElement(Text, null, "hola"),
-]),
- ]);
-};
-
-let {pipe} = ReactServerDOM.renderToPipeableStream(App());
-
-pipe(process.stdout);
diff --git a/arch/server/render-to-stream.js b/arch/server/render-html-to-stream.js
similarity index 98%
rename from arch/server/render-to-stream.js
rename to arch/server/render-html-to-stream.js
index d836a11cf..d49f50352 100644
--- a/arch/server/render-to-stream.js
+++ b/arch/server/render-html-to-stream.js
@@ -26,18 +26,10 @@ const debug = (readableStream) => {
reader.read().then(debugReader);
};
-/* const app = () => (
-
-
- "lol"
-
-
-); */
-
const sleep = (seconds) =>
new Promise((res) => setTimeout(res, seconds * 1000));
-const App = () => (
+/* const App = () => (
@@ -45,8 +37,17 @@ const App = () => (
+); */
+
+const App = () => (
+
+
+ "lol"
+
+
);
+
ReactDOM.renderToReadableStream().then((stream) => {
debug(stream);
});
diff --git a/arch/server/render-rsc-to-stream.js b/arch/server/render-rsc-to-stream.js
new file mode 100644
index 000000000..e789ec5af
--- /dev/null
+++ b/arch/server/render-rsc-to-stream.js
@@ -0,0 +1,40 @@
+import React from "react";
+import { renderToPipeableStream } from "react-server-dom-webpack/server";
+
+const DefferedComponent = async ({ sleep, children }) => {
+ await new Promise((res) => setTimeout(() => res(), sleep * 1000));
+ return Sleep {sleep}s, {children};
+};
+
+const decoder = new TextDecoder();
+
+const debug = (readableStream) => {
+ const reader = readableStream.getReader();
+ const debugReader = ({ done, value }) => {
+ if (done) {
+ console.log("Stream complete");
+ return;
+ }
+ console.log(decoder.decode(value));
+ console.log(" ");
+ return reader.read().then(debugReader);
+ };
+ reader.read().then(debugReader);
+};
+
+const sleep = (seconds) =>
+ new Promise((res) => setTimeout(res, seconds * 1000));
+
+const App = () => (
+
+
+
+ "lol"
+
+
+
+);
+
+const { pipe } = renderToPipeableStream();
+
+pipe(process.stdout);
diff --git a/demo/client/dune b/demo/client/dune
index 131c68ec9..cf95c7a3c 100644
--- a/demo/client/dune
+++ b/demo/client/dune
@@ -4,7 +4,7 @@
(enabled_if
(= %{profile} "dev"))
(modules index)
- (libraries melange demo_shared_js reason-react)
+ (libraries melange demo_shared_js reason-react melange.dom melange-webapi)
(preprocess
(pps reason-react-ppx browser_ppx -js melange.ppx))
(module_systems es6))
diff --git a/demo/client/index.re b/demo/client/index.re
index 0ad6d1a19..250c9f5ea 100644
--- a/demo/client/index.re
+++ b/demo/client/index.re
@@ -1,6 +1,8 @@
let _ = MelRaw.mockInitWebsocket();
-switch (ReactDOM.querySelector("#root")) {
+let element = Webapi.Dom.Document.querySelector("#root", Webapi.Dom.document);
+
+switch (element) {
| Some(el) =>
let _ = ReactDOM.Client.hydrateRoot(el, );
();
diff --git a/demo/client/runtime-with-client.jsx b/demo/client/runtime-with-client.jsx
index ccd90a278..6fc2e6650 100644
--- a/demo/client/runtime-with-client.jsx
+++ b/demo/client/runtime-with-client.jsx
@@ -1,11 +1,10 @@
window.__webpack_require__ = (id) => {
- const component = window.__client_manifest_map[id];
console.log("REQUIRE ---");
+ const component = window.__client_manifest_map[id];
console.log(id);
console.log(component);
console.log("---");
- /* return { __esModule: true, default: component }; */
- return component;
+ return { __esModule: true, default: component };
};
const React = require("react");
@@ -21,17 +20,20 @@ const register = (name, render) => {
};
register(
- "Note_editor",
- React.lazy(() => import("./app/demo/universal/js/Note_editor.js")),
+ "Counter",
+ React.lazy(() => import("./app/demo/universal/js/Counter.js"))
);
+
register(
- "Counter",
- React.lazy(() => import("./app/demo/universal/js/Counter.js")),
+ "Note_editor",
+ React.lazy(() => import("./app/demo/universal/js/Note_editor.js")),
);
-register(
+
+/* register(
"Promise_renderer",
React.lazy(() => import("./app/demo/universal/js/Promise_renderer.js")),
-);
+); */
+
/* end bootstrap.js */
class ErrorBoundary extends React.Component {
@@ -68,14 +70,17 @@ try {
const stream = window.srr_stream.readable_stream;
const promise = ReactServerDOM.createFromReadableStream(stream);
const element = document.getElementById("root");
- const app = (
-
-
-
- );
+
React.startTransition(() => {
+ const app = (
+
+
+
+ );
ReactDOM.hydrateRoot(element, app);
});
} catch (e) {
- console.error(e);
+ console.error("Error type:", e.constructor.name);
+ console.error("Full error:", e);
+ console.error("Stack:", e.stack);
}
diff --git a/demo/server/server.re b/demo/server/server.re
index e0f4e2b51..086b6fc67 100644
--- a/demo/server/server.re
+++ b/demo/server/server.re
@@ -74,12 +74,34 @@ let stream_rsc = fn => {
);
};
-let serverComponentsHandler = request => {
- let sleep = (~ms, value) => {
- let%lwt () = Lwt_unix.sleep(ms /. 1000.);
- Lwt.return(value);
+module Page = {
+ [@react.async.component]
+ let make = () => {
+ let%lwt () = Lwt_unix.sleep(1.0);
+ Lwt.return(
+
+
+
+ {React.string("This is a small form")}
+
+ /* TODO: payload is wrong in client components */
+
+
+
+
+
+ ,
+ );
};
- let app = ;
+};
+
+let serverComponentsHandler = request => {
+ let app = ;
switch (Dream.header(request, "Accept")) {
| Some(accept) when is_react_component_header(accept) =>
stream_rsc(stream => {
@@ -202,7 +224,6 @@ let router = [
let () = {
Dream.run(
- ~adjust_terminal=true,
~port=8080,
~interface={
switch (Sys.getenv_opt("SERVER_INTERFACE")) {
diff --git a/demo/universal/native/lib/App.re b/demo/universal/native/lib/App.re
index fe28d43ad..ab93c7328 100644
--- a/demo/universal/native/lib/App.re
+++ b/demo/universal/native/lib/App.re
@@ -90,7 +90,7 @@ let make = () => {
- print_endline("Clicked")} />
+ print_endline("Clicked")} />
;
};
diff --git a/demo/universal/native/lib/Counter.re b/demo/universal/native/lib/Counter.re
index 4e478f365..936c527ea 100644
--- a/demo/universal/native/lib/Counter.re
+++ b/demo/universal/native/lib/Counter.re
@@ -1,4 +1,4 @@
-let make = (~initial, ~onClick as [@browser_only] onClick) => {
+let make = (~initial, ~onClick as [@browser_only] onClick=?, ()) => {
let (count, [@browser_only] setCount) = RR.useStateValue(initial);
[@browser_only]
@@ -6,11 +6,14 @@ let make = (~initial, ~onClick as [@browser_only] onClick) => {
setCount(count + 1);
Js.log2("Printing count", count);
- onClick(e);
+ switch (onClick) {
+ | Some(onClick) => onClick(e)
+ | None => ()
+ };
};
-
+
{
-
- {React.string(
- "The HTML comes from the server"
- ++ " then is updated by the client after React runs. Via render or hydration (when using ReactDOM.hydrateRoot).",
- )}
-
;
};
[@react.component]
-let make = (~initial, ~onClick as [@browser_only] onClick) =>
+let make = (~initial, ~onClick as [@browser_only] onClick=?) =>
switch%platform (Runtime.platform) {
| Server =>
React.Client_component({
import_module: "Counter",
import_name: "",
props: [("initial", React.Json(`Int(initial)))],
- client: make(~initial, ~onClick=_ => ()),
+ client: make(~initial, ~onClick=_ => (), ()),
})
- | Client => make(~initial, ~onClick)
+ | Client => make(~initial, ~onClick?, ())
};
let default = make;
-
-/* switch%platform (Runtime.platform) {
- | Server => ()
- | Client =>
- Components.register("Counter", (props: Js.t({..})) => {
- React.jsx(make, makeProps(~initial=props##initial, ()))
- })
- };
- */
diff --git a/demo/universal/native/lib/Hr.re b/demo/universal/native/lib/Hr.re
new file mode 100644
index 000000000..c485ff6cd
--- /dev/null
+++ b/demo/universal/native/lib/Hr.re
@@ -0,0 +1,11 @@
+[@react.component]
+let make = () => {
+
;
+};
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/demo/universal/native/lib/Note_editor.re b/demo/universal/native/lib/Note_editor.re
index c4ef7fe53..c6120d68e 100644
--- a/demo/universal/native/lib/Note_editor.re
+++ b/demo/universal/native/lib/Note_editor.re
@@ -51,12 +51,3 @@ let make = (~title, ~body) =>
};
let default = make;
-
-/* switch%platform (Runtime.platform) {
- | Server => ()
- | Client =>
- Components.register("Note_editor", (props: Js.t({..})) => {
- React.jsx(make, makeProps(~title=props##title, ~body=props##body, ()))
- })
- };
- */
diff --git a/demo/universal/native/lib/Noter.re b/demo/universal/native/lib/Noter.re
deleted file mode 100644
index 058531756..000000000
--- a/demo/universal/native/lib/Noter.re
+++ /dev/null
@@ -1,38 +0,0 @@
-module Hr = {
- [@react.component]
- let make = () => {
-
;
- };
-};
-
-[@react.component]
-let make = (~valueIn3seconds as _) => {
- React.useEffect(() => {
- let _ = Js.log("Hello from the client");
- None;
- });
-
-
-
-
- {React.string("This is a small form")}
-
-
-
- print_endline("Clicked")} />
-
-
- ;
-};
diff --git a/demo/universal/native/lib/Promise_renderer.re b/demo/universal/native/lib/Promise_renderer.re
index 898d226ae..8fd8a4430 100644
--- a/demo/universal/native/lib/Promise_renderer.re
+++ b/demo/universal/native/lib/Promise_renderer.re
@@ -4,29 +4,24 @@ module Await = {
[@react.component]
let make = (~promise: Js.Promise.t(string)) => {
let value = React.Experimental.use(promise);
- {React.string("Promise resolved: " ++ value)}
;
+ React.string("[RESOLVED] " ++ value);
};
};
-let make = (~promise: Js.Promise.t(string)) =>
-
-
{React.string("Waiting for promise to resolve:")}
-
{React.string("Loading...")} }>
-
-
- ;
+let make = (~value: Js.Promise.t(string)) =>
+ {React.string("Promise: ")} ;
[@react.component]
-let make = (~promise) =>
+let make = (~value) =>
switch%platform (Runtime.platform) {
| Server =>
React.Client_component({
import_module: "Promise_renderer",
import_name: "",
- props: [("promise", React.Promise(promise, v => `String(v)))],
- client: make(~promise),
+ props: [("value", React.Promise(value, v => `String(v)))],
+ client: make(~value),
})
- | Client => make(~promise)
+ | Client => make(~value)
};
let default = make;
diff --git a/demo/universal/native/lib/Theme.re b/demo/universal/native/lib/Theme.re
index 64b94a5e6..ef6002906 100644
--- a/demo/universal/native/lib/Theme.re
+++ b/demo/universal/native/lib/Theme.re
@@ -32,5 +32,6 @@ module Color = {
let text = value => "text-" ++ value;
let background = value => "bg-" ++ value;
+let border = value => "border-" ++ value;
let hover = value => "hover:" ++ String.concat(" hover:", value);
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/packages/promise/js/promise.re b/packages/promise/js/promise.re
index 346bee0e3..6d6296004 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);
diff --git a/packages/reactDom/src/ReactServerDOM.ml b/packages/reactDom/src/ReactServerDOM.ml
index e7e4fe7f9..42ff860c8 100644
--- a/packages/reactDom/src/ReactServerDOM.ml
+++ b/packages/reactDom/src/ReactServerDOM.ml
@@ -14,45 +14,8 @@ module Fiber = struct
t.context.index <- t.context.index + 1;
t.context.index
+ let get_context t = t.context
(* let emit_html t html = t.emit_html html *)
-
- let root fn =
- let stream, push, close = Push_stream.make () in
- let initial_index = 0 in
- let context = { push; close; pending = 1; index = initial_index } in
- let htmls = ref [] in
- let finished, parent_done = Lwt.wait () in
- let emit_html chunk = htmls := chunk :: !htmls in
- let%lwt html = fn ({ context; emit_html; finished }, initial_index) in
- let shell = Html.list [ Html.list !htmls; html ] in
- Lwt.wakeup_later parent_done ();
- context.pending <- context.pending - 1;
- match context.pending = 0 with
- | true ->
- context.close ();
- Lwt.return (shell, None)
- | false -> Lwt.return (shell, Some stream)
-
- let task parent fn =
- let context = parent.context in
- let finished, parent_done = Lwt.wait () in
- match fn { context; emit_html = parent.emit_html; finished } with
- | `Fork (async, sync) ->
- context.pending <- context.pending + 1;
- parent.emit_html <- (fun html -> context.push html);
- Lwt.async (fun () ->
- let%lwt () = parent.finished in
- let%lwt html = async in
- context.push html;
- Lwt.wakeup_later parent_done ();
- context.pending <- context.pending - 1;
- if context.pending = 0 then context.close ();
- Lwt.return ());
- Lwt.return sync
- | `Sync sync ->
- Lwt.wakeup_later parent_done ();
- Lwt.return sync
- | `Fail exn -> Lwt.fail exn
end
module Model = struct
@@ -65,10 +28,12 @@ module Model = struct
mutable chunk_id : int;
}
- let use_index context =
+ let use_chunk_id context =
context.chunk_id <- context.chunk_id + 1;
context.chunk_id
+ let get_chunk_id context = context.chunk_id
+
let prop_to_json (prop : React.JSX.prop) =
(* TODO: Add promises/sets/others ??? *)
match prop with
@@ -96,7 +61,7 @@ module Model = struct
let promise_value id = Printf.sprintf "$@%x" id
let ref_value id = Printf.sprintf "$%x" id
- (* Not reusing node because we need to add fallback prop as json directly *)
+ (* Not reusing `node` because we need to add fallback prop as json directly *)
let suspense_node ~key ~fallback children : json =
let fallback_prop = ("fallback", fallback) in
let props =
@@ -111,7 +76,6 @@ module Model = struct
let component_ref ~module_ ~name =
let id = `String module_ in
- (* chunks is a webpack thing, we don't need it for now *)
let chunks = `List [] in
let component_name = `String name in
`List [ id; chunks; component_name ]
@@ -130,7 +94,7 @@ module Model = struct
Buffer.add_string buf "\n";
Buffer.contents buf
- let element_to_model ~context index element =
+ let element_to_model ~context element =
let rec to_payload element =
match (element : React.element) with
| Empty -> `Null
@@ -150,7 +114,7 @@ module Model = struct
let element = component () in
(* Instead of returning the payload directly, we push it, and return a reference to it.
This is how `react-server-dom-webpack/server` renderToPipeableStream works *)
- let index = use_index context in
+ let index = use_chunk_id context in
context.push index (Chunk_value (to_payload element));
`String (ref_value index)
| Async_component component -> (
@@ -159,7 +123,7 @@ module Model = struct
| Fail exn -> raise exn
| Return element -> to_payload element
| Sleep ->
- let index = use_index context in
+ let index = use_chunk_id context in
context.pending <- context.pending + 1;
Lwt.async (fun () ->
let%lwt element = promise in
@@ -173,7 +137,7 @@ module Model = struct
let fallback = to_payload fallback in
suspense_node ~key ~fallback [ to_payload children ]
| Client_component { import_module; import_name; props; client = _ } ->
- let id = use_index context in
+ let id = use_chunk_id context in
let ref = component_ref ~module_:import_module ~name:import_name in
context.push id (Chunk_component_ref ref);
let client_props = client_props_to_json props in
@@ -192,13 +156,13 @@ module Model = struct
| name, Promise (promise, value_to_json) -> (
match Lwt.state promise with
| Return value ->
- let chunk_id = use_index context in
+ let chunk_id = use_chunk_id context in
let json = value_to_json value in
(* TODO: Make sure why we need a chunk here *)
context.push context.chunk_id (Chunk_value json);
(name, `String (promise_value chunk_id))
| Sleep ->
- let chunk_id = use_index context in
+ let chunk_id = use_chunk_id context in
context.pending <- context.pending + 1;
Lwt.async (fun () ->
let%lwt value = promise in
@@ -213,18 +177,20 @@ module Model = struct
raise exn))
props
in
- context.push index (Chunk_value (to_payload element));
+ let initial_chunk_id = get_chunk_id context in
+ context.push initial_chunk_id (Chunk_value (to_payload element));
if context.pending = 0 then context.close ()
let render ?subscribe element : string Lwt_stream.t Lwt.t =
+ let initial_chunk_id = 0 in
let stream, push, close = Push_stream.make () in
let push_chunk id chunk =
match chunk with
| Chunk_value json -> push (model_to_chunk id json)
| Chunk_component_ref json -> push (client_reference_to_chunk id json)
in
- let context : stream_context = { push = push_chunk; close; chunk_id = 0; pending = 0 } in
- element_to_model ~context context.chunk_id element;
+ let context : stream_context = { push = push_chunk; close; chunk_id = initial_chunk_id; pending = 0 } in
+ element_to_model ~context element;
(* TODO: Currently returns the stream because of testing, in the future we can use subscribe to capture all chunks *)
match subscribe with
| None -> Lwt.return stream
@@ -240,8 +206,8 @@ let rsc_start_script =
{|
let enc = new TextEncoder();
let srr_stream = (window.srr_stream = {});
-srr_stream.push = (value) => {
- srr_stream._c.enqueue(enc.encode(value))
+srr_stream.push = () => {
+ srr_stream._c.enqueue(enc.encode(document.currentScript.dataset.payload));
};
srr_stream.close = () => {
srr_stream._c.close();
@@ -258,13 +224,12 @@ let rc_function_definition =
let rc_function_script = Html.node "script" [] [ Html.raw rc_function_definition ]
let chunk_script script =
- Html.node "script"
- [ Html.attribute "data-payload" (Html.single_quote_escape script) ]
- [ Html.raw "window.srr_stream.push(document.currentScript.dataset.payload);" ]
+ Html.raw
+ (Printf.sprintf "" (Html.single_quote_escape script))
let client_reference_chunk_script index json = chunk_script (Model.client_reference_to_chunk index json)
let client_value_chunk_script index json = chunk_script (Model.model_to_chunk index json)
-let chunk_stream_end_script = Html.node "script" [] [ Html.raw "window.srr_stream.close();" ]
+let chunk_stream_end_script = Html.node "script" [] [ Html.raw "window.srr_stream.close()" ]
let rc_replacement b s = Html.node "script" [] [ Html.raw (Printf.sprintf "$RC('B:%x', 'S:%x')" b s) ]
let chunk_html_script index html =
@@ -304,8 +269,9 @@ let rec client_to_html ~fiber (element : React.element) =
let rec wait_for_suspense_to_resolve () =
match component () with
| exception React.Suspend (Any_promise promise) ->
- let%lwt _ = promise in
- wait_for_suspense_to_resolve ()
+ let open Lwt.Infix in
+ promise >>= fun _ -> wait_for_suspense_to_resolve ()
+ | exception _exn -> Lwt.return Html.null
| output ->
(* TODO: Do we need to care about batching? *)
client_to_html ~fiber output
@@ -318,11 +284,22 @@ let rec client_to_html ~fiber (element : React.element) =
| Suspense { key = _; children; fallback } ->
(* TODO: Do we need to care if there's Any_promise raising ? *)
let%lwt fallback = client_to_html ~fiber fallback in
- Fiber.task fiber (fun fiber ->
- let index = Fiber.use_index fiber in
- let async = children |> client_to_html ~fiber |> Lwt.map (chunk_html_script index) in
- let fallback_as_placeholder = html_suspense_placeholder ~fallback index in
- `Fork (async, fallback_as_placeholder))
+ let context = Fiber.get_context fiber in
+ let _finished, parent_done = Lwt.wait () in
+ let index = Fiber.use_index fiber in
+ let async = children |> client_to_html ~fiber |> Lwt.map (chunk_html_script index) in
+ let sync = html_suspense_placeholder ~fallback index in
+ context.pending <- context.pending + 1;
+ fiber.emit_html <- (fun html -> context.push html);
+ Lwt.async (fun () ->
+ let%lwt () = fiber.finished in
+ let%lwt html = async in
+ context.push html;
+ Lwt.wakeup_later parent_done ();
+ context.pending <- context.pending - 1;
+ if context.pending = 0 then context.close ();
+ Lwt.return ());
+ Lwt.return sync
| Client_component { import_module = _; import_name = _; props = _; client } -> client_to_html ~fiber client
(* TODO: Need to do something for those? *)
| Provider children -> client_to_html ~fiber children
@@ -357,7 +334,8 @@ let rec to_html ~fiber (element : React.element) : (Html.element * json) Lwt.t =
let%lwt _html, model = to_html ~fiber element in
Lwt.return (name, model)
| Promise (promise, value_to_json) ->
- Fiber.task fiber @@ fun fiber ->
+ let context = Fiber.get_context fiber in
+ let _finished, parent_done = Lwt.wait () in
let index = Fiber.use_index fiber in
let sync = (name, `String (Model.promise_value index)) in
let async : Html.element Lwt.t =
@@ -366,42 +344,54 @@ let rec to_html ~fiber (element : React.element) : (Html.element * json) Lwt.t =
let ret = chunk_script (Model.client_reference_to_chunk index json) in
Lwt.return ret
in
- `Fork (async, sync)
+ context.pending <- context.pending + 1;
+ fiber.emit_html <- (fun html -> context.push html);
+ Lwt.async (fun () ->
+ let%lwt () = fiber.finished in
+ let%lwt html = async in
+ context.push html;
+ Lwt.wakeup_later parent_done ();
+ context.pending <- context.pending - 1;
+ if context.pending = 0 then context.close ();
+ Lwt.return ());
+ Lwt.return sync
| Json json -> Lwt.return (name, json))
props
in
let lwt_html = client_to_html ~fiber client in
- (* NOTE: this Lwt.pause () is important as we resolve client component in
- an async way we need to suspend above, otherwise React.js runtime won't work *)
let%lwt () = Lwt.pause () in
+ let index = Fiber.use_index fiber in
+ let ref : json = Model.component_ref ~module_:import_module ~name:import_name in
+ fiber.emit_html (client_reference_chunk_script index ref);
let%lwt html, props = Lwt.both lwt_html lwt_props in
- let model =
- let index = Fiber.use_index fiber in
- let ref : json = Model.component_ref ~module_:import_module ~name:import_name in
- fiber.emit_html (client_reference_chunk_script index ref);
- Model.node ~tag:(Model.ref_value index) ~key:None ~props []
- in
+ let model = Model.node ~tag:(Model.ref_value index) ~key:None ~props [] in
Lwt.return (html, model)
- | Suspense { key; children; fallback } ->
+ | Suspense { key; children; fallback } -> (
let%lwt html_fallback, model_fallback = to_html ~fiber fallback in
- Fiber.task fiber (fun fiber ->
- let promise = to_html ~fiber children in
- match Lwt.state promise with
- | Lwt.Sleep ->
- let index = Fiber.use_index fiber in
- let async_html =
- let%lwt html, model = promise in
- Lwt.return (Html.list [ chunk_html_script index html; client_value_chunk_script index model ])
- in
- let sync_html =
- ( html_suspense_placeholder ~fallback:html_fallback index,
- Model.suspense_placeholder ~key ~fallback:model_fallback index )
- in
- `Fork (async_html, sync_html)
- | Lwt.Return (html, model) ->
- let model = Model.suspense_node ~key ~fallback:model_fallback [ model ] in
- `Sync (html_suspense html, model)
- | Lwt.Fail exn -> `Fail exn)
+ let context = fiber.context in
+ let _finished, parent_done = Lwt.wait () in
+ let promise = to_html ~fiber children in
+ match Lwt.state promise with
+ | Lwt.Sleep ->
+ let index = Fiber.use_index fiber in
+ context.pending <- context.pending + 1;
+ fiber.emit_html <- (fun html -> context.push html);
+ Lwt.async (fun () ->
+ let%lwt () = fiber.finished in
+ let%lwt html, _model = promise in
+ context.push html;
+ Lwt.wakeup_later parent_done ();
+ context.pending <- context.pending - 1;
+ if context.pending = 0 then context.close ();
+ Lwt.return ());
+ Lwt.return
+ ( html_suspense_placeholder ~fallback:html_fallback index,
+ Model.suspense_placeholder ~key ~fallback:model_fallback index )
+ | Lwt.Return (html, model) ->
+ let model = Model.suspense_node ~key ~fallback:model_fallback [ model ] in
+ Lwt.wakeup_later parent_done ();
+ Lwt.return (html_suspense html, model)
+ | Lwt.Fail exn -> Lwt.fail exn)
| Provider children -> to_html ~fiber children
| Consumer children -> to_html ~fiber children
(* TODO: There's a task to remove InnerHtml in ReactDOM and use Html.raw directly. Here is still unclear what do to since we assing dangerouslySetInnerHTML to the right prop on the model. Also, should this model be `Null? *)
@@ -412,6 +402,8 @@ and elements_to_html ~fiber elements =
let htmls, model = List.split html_and_models in
Lwt.return (Html.list htmls, `List model)
+(* TODO: We could use only the Async case, where head and shell handle all the sync while subscribe handles the async,
+ also we might want to implement "resources" instead of head. *)
type rendering =
| Done of { head : Html.element; body : Html.element; end_script : Html.element }
| Async of { head : Html.element; shell : Html.element; subscribe : (Html.element -> unit Lwt.t) -> unit Lwt.t }
@@ -420,11 +412,24 @@ type rendering =
(* TODO: Do we need to disable the model rendering? Can we do something better than a boolean? *)
(* TODO: Add scripts and links to the output, also all options from renderToReadableStream *)
let render_to_html element =
+ let initial_index = 0 in
+ let htmls = ref [] in
+ let emit_html chunk = htmls := chunk :: !htmls in
+ let stream, push, close = Push_stream.make () in
+ let context : Fiber.context = { push; close; pending = 1; index = initial_index } in
+ let finished, parent_done = Lwt.wait () in
+ let fiber : Fiber.t = { context; emit_html; finished } in
+ let%lwt root_html, root_model = to_html ~fiber element in
+ let root_chunk = client_value_chunk_script initial_index root_model in
+ let shell = Html.list [ Html.list !htmls; root_html; root_chunk ] in
+ Lwt.wakeup_later parent_done ();
+ context.pending <- context.pending - 1;
let%lwt html_shell, html_async =
- Fiber.root (fun (fiber, index) ->
- let%lwt html, model = to_html ~fiber element in
- let first_chunk = client_value_chunk_script index model in
- Lwt.return (Html.list [ html; first_chunk ]))
+ match context.pending = 0 with
+ | true ->
+ context.close ();
+ Lwt.return (shell, None)
+ | false -> Lwt.return (shell, Some stream)
in
match html_async with
| None -> Lwt.return (Done { head = rsc_start_script; body = html_shell; end_script = chunk_stream_end_script })
diff --git a/packages/reactDom/test/dune b/packages/reactDom/test/dune
index 2321fffe6..cbc7af101 100644
--- a/packages/reactDom/test/dune
+++ b/packages/reactDom/test/dune
@@ -7,6 +7,7 @@
lwt
server-reason-react.react
server-reason-react.reactDom
+ server-reason-react.html
server-reason-react.js
alcotest
yojson
diff --git a/packages/reactDom/test/test.ml b/packages/reactDom/test/test.ml
index 9a7f740ab..7760aa10c 100644
--- a/packages/reactDom/test/test.ml
+++ b/packages/reactDom/test/test.ml
@@ -7,5 +7,6 @@ let () =
Test_renderToStaticMarkup.tests;
Test_renderToString.tests;
Test_reactDOMStyle.tests;
- Test_ReactServerDOM.tests;
+ Test_RSC_model.tests;
+ Test_RSC_html.tests;
]))
diff --git a/packages/reactDom/test/test_RSC_html.ml b/packages/reactDom/test/test_RSC_html.ml
new file mode 100644
index 000000000..4beb15ab2
--- /dev/null
+++ b/packages/reactDom/test/test_RSC_html.ml
@@ -0,0 +1,168 @@
+let yojson = Alcotest.testable Yojson.Safe.pretty_print ( = )
+let check_json = Alcotest.check yojson "should be equal"
+let assert_json left right = Alcotest.check yojson "should be equal" right left
+
+let assert_list (type a) (ty : a Alcotest.testable) (left : a list) (right : a list) =
+ Alcotest.check (Alcotest.list ty) "should be equal" right left
+
+let assert_list_of_strings (left : string list) (right : string list) =
+ Alcotest.check (Alcotest.list Alcotest.string) "should be equal" right left
+
+let test title fn =
+ ( Printf.sprintf "ReactServerDOM.render_to_html / %s" title,
+ [
+ Alcotest_lwt.test_case "" `Quick (fun _switch () ->
+ let start = Unix.gettimeofday () in
+ let timeout =
+ let%lwt () = Lwt_unix.sleep 3.0 in
+ Alcotest.failf "Test '%s' timed out" title
+ in
+ let%lwt test_promise = Lwt.pick [ fn (); timeout ] in
+ let epsilon = 0.001 in
+ let duration = Unix.gettimeofday () -. start in
+ if abs_float duration >= epsilon then
+ Printf.printf "\027[1m\027[33m[WARNING]\027[0m Test '%s' took %.3f seconds\n" title duration
+ else ();
+ Lwt.return test_promise);
+ ] )
+
+let assert_string left right = Alcotest.check Alcotest.string "should be equal" right left
+
+let assert_stream (stream : string Lwt_stream.t) (expected : string list) =
+ let%lwt content = Lwt_stream.to_list stream in
+ if content = [] then Lwt.return @@ Alcotest.fail "stream should not be empty"
+ else Lwt.return @@ assert_list_of_strings content expected
+
+let assert_html (left : Html.element) (right : string) = assert_string (Html.to_string left) right
+
+let text_encoder_script =
+ ""
+
+let rc_function_script =
+ ""
+ ^ text_encoder_script
+
+let stream_close_script = ""
+
+let assert_sync_payload app sync_body =
+ match%lwt ReactServerDOM.render_to_html app with
+ | Done { head; body; end_script } ->
+ assert_html head text_encoder_script;
+ assert_html body sync_body;
+ assert_html end_script stream_close_script;
+ Lwt.return ()
+ | Async _ -> Lwt.return (Alcotest.fail "Async should not be returned by render_to_html")
+
+let assert_html_list (elements : Html.element list) (expected : string list) =
+ assert_list_of_strings (List.map Html.to_string elements) expected
+
+let assert_async_payload element ~shell assertion_list =
+ match%lwt ReactServerDOM.render_to_html element with
+ | Done _ -> Lwt.return (Alcotest.fail "Sync should be returned by render_to_html")
+ | Async { head; shell = outcome_shell; subscribe } ->
+ assert_html head rc_function_script;
+ assert_html outcome_shell shell;
+ let subscribed_elements = ref [] in
+ let%lwt () =
+ subscribe (fun element ->
+ subscribed_elements := !subscribed_elements @ [ element ];
+ Lwt.return ())
+ in
+ assert_html_list subscribed_elements.contents assertion_list;
+ Lwt.return ()
+
+let layout ~children () =
+ React.Upper_case_component
+ (fun () -> React.createElement "div" [] [ React.createElement "p" [] [ React.string "Awesome webpage"; children ] ])
+
+let loading_suspense ~children () = React.Suspense.make ~fallback:(React.string "Loading...") ~children ()
+
+(* ***** *)
+(* Tests *)
+(* ***** *)
+
+let null_element () =
+ let app = React.null in
+ assert_sync_payload app ""
+
+let upper_case_component () =
+ let app =
+ React.Upper_case_component
+ (fun () ->
+ React.createElement "div" []
+ [
+ React.createElement "section" [] [ React.createElement "article" [] [ React.string "Deep Server Content" ] ];
+ ])
+ in
+ assert_sync_payload app
+ ""
+
+let async_component_without_promise () =
+ let app =
+ React.Async_component
+ (fun () ->
+ Lwt.return
+ (React.createElement "div" []
+ [
+ React.createElement "section" []
+ [ React.createElement "article" [] [ React.string "Deep Server Content" ] ];
+ ]))
+ in
+ assert_sync_payload app
+ ""
+
+let suspense_without_promise () =
+ let app () = loading_suspense ~children:(React.string "Resolved") () in
+ assert_sync_payload (app ())
+ "Resolved"
+
+let with_sleepy_promise () =
+ let app =
+ loading_suspense
+ ~children:
+ (React.Async_component
+ (fun () ->
+ let%lwt () = Lwt_unix.sleep 0.1 in
+ Lwt.return
+ (React.createElement "div" []
+ [
+ React.createElement "section" []
+ [ React.createElement "article" [] [ React.string "Deep Server Content" ] ];
+ ])))
+ in
+ assert_async_payload (app ())
+ ~shell:
+ "Loading..."
+ [ ""; stream_close_script ]
+
+let tests =
+ [
+ test "null_element" null_element;
+ test "upper_case_component" upper_case_component;
+ test "async_component_without_promise" async_component_without_promise;
+ test "suspense_without_promise" suspense_without_promise;
+ test "with_sleepy_promise" with_sleepy_promise;
+ ]
diff --git a/packages/reactDom/test/test_ReactServerDOM.ml b/packages/reactDom/test/test_RSC_model.ml
similarity index 84%
rename from packages/reactDom/test/test_ReactServerDOM.ml
rename to packages/reactDom/test/test_RSC_model.ml
index 4a9982460..3703cba53 100644
--- a/packages/reactDom/test/test_ReactServerDOM.ml
+++ b/packages/reactDom/test/test_RSC_model.ml
@@ -31,16 +31,6 @@ let assert_stream (stream : string Lwt_stream.t) (expected : string list) =
if content = [] then Lwt.return @@ Alcotest.fail "stream should not be empty"
else Lwt.return @@ assert_list_of_strings content expected
-let test_silly_stream () =
- let stream, push = Lwt_stream.create () in
- push (Some "first");
- let%lwt () = Lwt_unix.sleep 0.1 in
- push (Some "secondo");
- let%lwt () = Lwt_unix.sleep 0.1 in
- push (Some "trienio");
- push None;
- assert_stream stream [ "first"; "secondo"; "trienio" ]
-
let null_element () =
let app = React.null in
let%lwt stream = ReactServerDOM.render_to_model app in
@@ -168,20 +158,39 @@ let suspense_without_promise () =
"0:\"$1\"\n";
]
-let immediate_suspense () =
- let suspended_component =
+let suspense_with_promise () =
+ let app () =
+ React.Suspense.make ~fallback:(React.string "Loading...")
+ ~children:
+ (React.Async_component
+ (fun () ->
+ let%lwt () = Lwt_unix.sleep 1.0 in
+ Lwt.return (React.string "lol")))
+ ()
+ in
+ let main = React.Upper_case_component app in
+ let%lwt stream = ReactServerDOM.render_to_model main in
+ assert_stream stream
+ [
+ "1:[\"$\",\"$Sreact.suspense\",null,{\"fallback\":\"Loading...\",\"children\":\"$L2\"}]\n";
+ "0:\"$1\"\n";
+ "2:\"lol\"\n";
+ ]
+
+let suspense_with_immediate_promise () =
+ let resolved_component =
React.Async_component
(fun () ->
let value = "DONE :)" in
Lwt.return (React.string value))
in
- let app () = React.Suspense.make ~fallback:(React.string "Loading...") ~children:suspended_component () in
+ let app = React.Suspense.make ~fallback:(React.string "Loading...") ~children:resolved_component in
let main = React.Upper_case_component app in
let%lwt stream = ReactServerDOM.render_to_model main in
assert_stream stream
[ "1:[\"$\",\"$Sreact.suspense\",null,{\"fallback\":\"Loading...\",\"children\":\"DONE :)\"}]\n"; "0:\"$1\"\n" ]
-let delayed_value ?(ms = 100) value =
+let delayed_value ~ms value =
let%lwt () = Lwt_unix.sleep (Int.to_float ms /. 100.0) in
Lwt.return value
@@ -189,9 +198,8 @@ let suspense () =
let suspended_component =
React.Async_component
(fun () ->
- let open Lwt.Syntax in
- let+ value = delayed_value "DONE :)" in
- React.string value)
+ let%lwt value = delayed_value ~ms:100 "DONE :)" in
+ Lwt.return (React.string value))
in
let app () = React.Suspense.make ~fallback:(React.string "Loading...") ~children:suspended_component () in
let main = React.Upper_case_component app in
@@ -203,6 +211,44 @@ let suspense () =
"2:\"DONE :)\"\n";
]
+let nested_suspense () =
+ let deffered_component =
+ React.Async_component
+ (fun () ->
+ let%lwt value = delayed_value ~ms:200 "DONE :)" in
+ Lwt.return (React.string value))
+ in
+ let app () = React.Suspense.make ~fallback:(React.string "Loading...") ~children:deffered_component () in
+ let main = React.Upper_case_component app in
+ let%lwt stream = ReactServerDOM.render_to_model main in
+ assert_stream stream
+ [
+ "1:[\"$\",\"$Sreact.suspense\",null,{\"fallback\":\"Loading...\",\"children\":\"$L2\"}]\n";
+ "0:\"$1\"\n";
+ "2:\"DONE :)\"\n";
+ ]
+
+let async_component_without_suspense () =
+ (* Because there's no Suspense. We await for the promise to resolve before rendering the component *)
+ let app =
+ React.Async_component
+ (fun () ->
+ let%lwt value = delayed_value ~ms:100 "DONE :)" in
+ Lwt.return (React.string value))
+ in
+ let%lwt stream = ReactServerDOM.render_to_model app in
+ assert_stream stream [ "0:\"$L1\"\n"; "1:\"DONE :)\"\n" ]
+
+let async_component_without_suspense_immediate () =
+ let app =
+ React.Async_component
+ (fun () ->
+ let%lwt value = delayed_value ~ms:0 "DONE :)" in
+ Lwt.return (React.string value))
+ in
+ let%lwt stream = ReactServerDOM.render_to_model app in
+ assert_stream stream [ "0:\"$L1\"\n"; "1:\"DONE :)\"\n" ]
+
let client_without_props () =
let app () =
React.Upper_case_component
@@ -351,8 +397,11 @@ let tests =
test "upper_case_with_list" upper_case_with_list;
test "upper_case_with_children" upper_case_with_children;
test "suspense_without_promise" suspense_without_promise;
- test "immediate_suspense" immediate_suspense;
+ test "suspense_with_promise" suspense_with_promise;
+ test "suspense_with_immediate_promise" suspense_with_immediate_promise;
test "suspense" suspense;
+ test "async_component_without_suspense" async_component_without_suspense;
+ test "async_component_without_suspense_immediate" async_component_without_suspense_immediate;
test "mixed_server_and_client" mixed_server_and_client;
test "client_with_json_props" client_with_json_props;
test "client_without_props" client_without_props;
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 78129e59e..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 transform_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 (transform_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 = ;
- assert_string(ReactDOM.renderToStaticMarkup(div), {||});
+ assert_string(
+ ReactDOM.renderToStaticMarkup(),
+ {||},
+ );
};
let bool_attributes = () => {
@@ -68,15 +68,15 @@ let link_as_attribute = () => {
};
let innerhtml_attribute = () => {
- let div = ;
- assert_string(ReactDOM.renderToStaticMarkup(div), {|foo
|});
+ let app = ;
+ assert_string(ReactDOM.renderToStaticMarkup(app), {|foo
|});
};
let innerhtml_attribute_complex = () => {
- let div =
+ let app =
;
assert_string(
- ReactDOM.renderToStaticMarkup(div),
+ ReactDOM.renderToStaticMarkup(app),
{|console.log("Lola")
|},
);
};
@@ -97,21 +97,21 @@ let int_opt_attribute_none = () => {
};
let fragment = () => {
- let div = <> >;
+ let app = <> >;
assert_string(
- ReactDOM.renderToStaticMarkup(div),
+ ReactDOM.renderToStaticMarkup(app),
{||},
);
};
let fragment_with_key = () => {
- let div =
+ let app =
;
assert_string(
- ReactDOM.renderToStaticMarkup(div),
+ ReactDOM.renderToStaticMarkup(app),
{||},
);
};
@@ -380,49 +380,94 @@ let optional_prop = () => {
);
};
-let _ =
- Alcotest.run(
- "server-reason-react.ppx",
- [
- (
- "renderToStaticMarkup",
- [
- test("div", tag),
- test("div_empty_attr", empty_attribute),
- test("div_bool_attr", bool_attribute),
- test("input_bool_attrs", bool_attributes),
- test("p_inner_html", innerhtml),
- test("div_int_attr", int_attribute),
- test("svg_1", svg_1),
- test("svg_2", svg_2),
- test("booleanish_props_with_ppx", booleanish_props_with_ppx),
- test("booleanish_props_without_ppx", booleanish_props_without_ppx),
- test("style_attr", style_attribute),
- test("div_ref_attr", ref_attribute),
- test("link_as_attr", link_as_attribute),
- test("inner_html_attr", innerhtml_attribute),
- test("p_inner_html", innerhtml_attribute_complex),
- test("int_opt_attr_some", int_opt_attribute_some),
- test("int_opt_attr_none", int_opt_attribute_none),
- test("string_opt_attr_some", string_opt_attribute_some),
- test("string_opt_attr_none", string_opt_attribute_none),
- test("bool_opt_attr_some", bool_opt_attribute_some),
- test("bool_opt_attr_none", bool_opt_attribute_none),
- test("style_opt_attr_some", style_opt_attribute_some),
- test("style_opt_attr_none", style_opt_attribute_none),
- test("ref_opt_attr_some", ref_opt_attribute_some),
- test("ref_opt_attr_none", ref_opt_attribute_none),
- test("test_fragment", fragment),
- test("test_fragment_with_key", fragment_with_key),
- test("test_children_uppercase", children_uppercase),
- test("test_children_lowercase", children_lowercase),
- test("event_onClick", onClick_empty),
- test("children_one_element", children_one_element),
- test("children_multiple_elements", children_multiple_elements),
- test("createElementVariadic", create_element_variadic),
- test("aria_props", aria_props),
- test("optional_prop", optional_prop),
- ],
- ),
- ],
+let context = React.createContext(10);
+
+module ContextProvider = {
+ include React.Context;
+ let make = React.Context.provider(context);
+};
+
+module ContextConsumer = {
+ [@react.component]
+ let make = () => {
+ let value = React.useContext(context);
+ ;
+ };
+};
+
+let context = () => {
+ let component =
+ ;
+
+ assert_string(
+ ReactDOM.renderToStaticMarkup(component),
+ "",
+ );
+};
+
+let context_2 = () => {
+ let component =
+ ;
+
+ assert_string(
+ ReactDOM.renderToStaticMarkup(component),
+ "",
);
+};
+
+let multiple_contexts = () => {
+ let _component =
+ ;
+
+ let component =
+ ;
+
+ assert_string(
+ ReactDOM.renderToStaticMarkup(component),
+ "",
+ );
+};
+
+Alcotest.run(
+ "server-reason-react.ppx",
+ [
+ test("tag", tag),
+ test("empty_attribute", empty_attribute),
+ test("bool_attribute", bool_attribute),
+ test("bool_attributes", bool_attributes),
+ test("innerhtml", innerhtml),
+ test("int_attribute", int_attribute),
+ test("svg_1", svg_1),
+ test("svg_2", svg_2),
+ test("booleanish_props_with_ppx", booleanish_props_with_ppx),
+ test("booleanish_props_without_ppx", booleanish_props_without_ppx),
+ test("style_attribute", style_attribute),
+ test("ref_attribute", ref_attribute),
+ test("link_as_attribute", link_as_attribute),
+ test("inner_html_attribute", innerhtml_attribute),
+ test("inner_html_attribute_complex", innerhtml_attribute_complex),
+ test("int_opt_attr_some", int_opt_attribute_some),
+ test("int_opt_attribute_none", int_opt_attribute_none),
+ test("string_opt_attribute_some", string_opt_attribute_some),
+ test("string_opt_attribute_none", string_opt_attribute_none),
+ test("bool_opt_attribute_some", bool_opt_attribute_some),
+ test("bool_opt_attribute_none", bool_opt_attribute_none),
+ test("style_opt_attribute_some", style_opt_attribute_some),
+ test("style_opt_attribute_none", style_opt_attribute_none),
+ test("ref_opt_attribute_some", ref_opt_attribute_some),
+ test("ref_opt_attribute_none", ref_opt_attribute_none),
+ test("fragment", fragment),
+ test("fragment_with_key", fragment_with_key),
+ test("children_uppercase", children_uppercase),
+ test("children_lowercase", children_lowercase),
+ test("event_onClick", onClick_empty),
+ test("children_one_element", children_one_element),
+ test("children_multiple_elements", children_multiple_elements),
+ test("create_element_variadic", create_element_variadic),
+ test("aria_props", aria_props),
+ test("optional_prop", optional_prop),
+ test("context", context),
+ test("context_2", context_2),
+ test("multiple_contexts", multiple_contexts),
+ ],
+);
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"}