Skip to content
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
34 changes: 29 additions & 5 deletions arch/server/render-rsc-to-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,37 @@ let Await_tick = ({ num }) => {
return num
}

const App = () => {
const Bar = () => {
return (
<div className="container">
<link rel="stylesheet" href="bootstrap.min.css" precedence="high" /></div>
<div>
Bar
</div>
)
};
const { pipe } = renderToPipeableStream(<App />);
}

const Foo = () => {
return (
<Bar />
)
}
// Mimic of @file_context_0 – suspense with two “Text” children.

const Text = ({ children }) => <span>{children}</span>;

const App = () => (
<React.Suspense fallback={"Loading..."}>
<div>
<Text>hi</Text>
<Text>hola</Text>
</div>
</React.Suspense>
);
const { pipe } = renderToPipeableStream(<React.Suspense fallback={"Loading..."}>
<div>
<Text>hi</Text>
<Text>hola</Text>
</div>
</React.Suspense>);

pipe(process.stdout);

Expand Down
74 changes: 29 additions & 45 deletions packages/reactDom/src/ReactServerDOM.ml
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,9 @@ module Model = struct
context.push (to_chunk (Debug_ref (`String debug_info_ref)) current_index) |> ignore
else ()

let rec element_to_payload ~context ?(debug = false) ~to_chunk ~env element =
let is_root = ref true in

let rec turn_element_into_payload ~context element =
let rec element_to_payload ?(debug = false) ?(task = 0) ~context ~to_chunk ~env element =
let owner_task = ref task in
let rec turn_element_into_payload ~context ~task element =
match (element : React.element) with
| Empty -> `Null
| DangerouslyInnerHtml _ ->
Expand All @@ -272,38 +271,26 @@ module Model = struct
attributes
in
let props = props_to_json attributes in
node ~key ~tag ~props (List.map (turn_element_into_payload ~context) children)
| Fragment children ->
if is_root.contents then is_root := false;
turn_element_into_payload ~context children
| List children ->
if is_root.contents then is_root := false;
`List (List.map (turn_element_into_payload ~context) children)
| Array children ->
if is_root.contents then is_root := false;
`List (Array.map (turn_element_into_payload ~context) children |> Array.to_list)
if !owner_task <> task && is_dev env then (
let payload = node ~key ~tag ~props (List.map (turn_element_into_payload ~context ~task) children) in
let index = Stream.push ~context (to_chunk (Value payload)) in
owner_task := task;
`String (ref_value index))
else
let payload = node ~key ~tag ~props (List.map (turn_element_into_payload ~context ~task) children) in
payload
| Fragment children -> turn_element_into_payload ~context ~task children
| List children -> `List (List.map (turn_element_into_payload ~context ~task) children)
| Array children -> `List (Array.map (turn_element_into_payload ~context ~task) children |> Array.to_list)
| Upper_case_component (name, component) -> (
(* TODO: Get the stack info from component *)
match component () with
| element ->
(* TODO: Can we remove the is_root difference. It currently align with react.js behavior, but it's not clear what is the purpose of it *)
if is_root.contents then (
is_root := false;
(*
If it's the root element, React returns the element payload instead of a reference value.
Root is a special case: https://github.com/facebook/react/blob/f3a803617ec4ba9d14bf5205ffece28ed1496a1d/packages/react-server/src/ReactFlightServer.js#L756-L766
*)
if debug then push_debug_info ~context ~to_chunk ~env ~index:0 ~ownerName:name else ();
turn_element_into_payload ~context element)
else
(* If it's not the root React push the element to the stream and return the reference value *)
let element_index =
Stream.push ~context (fun index ->
if debug then push_debug_info ~context ~to_chunk ~env ~index ~ownerName:name else ();
let payload = turn_element_into_payload ~context element in
to_chunk (Value payload) index)
in
`String (ref_value element_index)
if debug then push_debug_info ~context ~to_chunk ~env ~index:task ~ownerName:name else ();
let payload = turn_element_into_payload ~context ~task element in
incr owner_task;
payload
| exception exn ->
let error = exn_to_error exn in
let index = Stream.push ~context (to_chunk (Error (env, error))) in
Expand All @@ -318,12 +305,12 @@ module Model = struct
let error = make_error ~message ~stack ~digest:"" in
let index = Stream.push ~context (to_chunk (Error (env, error))) in
`String (lazy_value index)
| Return element -> turn_element_into_payload ~context element
| Return element -> turn_element_into_payload ~context ~task element
| Sleep ->
let promise =
try%lwt
let%lwt element = promise in
Lwt.return (to_chunk (Value (turn_element_into_payload ~context element)))
Lwt.return (to_chunk (Value (turn_element_into_payload ~context ~task element)))
with exn ->
let message = Printexc.to_string exn in
let stack = create_stack_trace () in
Expand All @@ -335,29 +322,26 @@ module Model = struct
| Suspense { key; children; fallback } ->
(* TODO: Need to check is_root? *)
(* TODO: Maybe we need to push suspense index and suspense node separately *)
let fallback = turn_element_into_payload ~context fallback in
suspense_node ~key ~fallback [ turn_element_into_payload ~context children ]
let fallback = turn_element_into_payload ~context ~task fallback in
suspense_node ~key ~fallback [ turn_element_into_payload ~context ~task children ]
| Client_component { import_module; import_name; props; client = _ } ->
let ref = component_ref ~module_:import_module ~name:import_name in
let index = Stream.push ~context (to_chunk (Component_ref ref)) in
let client_props = client_values_to_json ~context ~to_chunk ~env props in
node ~tag:(ref_value index) ~props:client_props []
(* TODO: Do we need to do anything with Provider and Consumer? *)
| Provider children -> turn_element_into_payload ~context children
| Consumer children -> turn_element_into_payload ~context children
| Provider children -> turn_element_into_payload ~context ~task children
| Consumer children -> turn_element_into_payload ~context ~task children
in
turn_element_into_payload ~context element
turn_element_into_payload ~context ~task:!owner_task element

and client_value_to_json ~context ?debug ~to_chunk ~env value =
and client_value_to_json ?debug ?task ~context ~to_chunk ~env value =
match (value : React.client_value) with
| Json json -> json
| Error error ->
let index = Stream.push ~context (to_chunk (Error (env, error))) in
`String (error_value index)
| Element element ->
let payload = element_to_payload ~context ?debug ~to_chunk ~env element in
let index = Stream.push ~context (to_chunk (Value payload)) in
`String (ref_value index)
| Element element -> element_to_payload ?debug ?task ~context ~to_chunk ~env element
| Promise (promise, value_to_json) -> (
match Lwt.state promise with
| Return value ->
Expand All @@ -384,8 +368,8 @@ module Model = struct
let index = Stream.push ~context (to_chunk (Value (`Assoc [ ("id", `String action.id); ("bound", `Null) ]))) in
`String (action_value index)

and client_values_to_json ~context ~to_chunk ~env props =
List.map (fun (name, value) -> (name, client_value_to_json ~context ~to_chunk ~env value)) props
and client_values_to_json ?task ~context ~to_chunk ~env props =
List.map (fun (name, value) -> (name, client_value_to_json ?task ~context ~to_chunk ~env value)) props

let render ?(env = `Dev) ?(debug = false) ?subscribe element =
let stream, context = Stream.make ~initial_index:0 in
Expand Down
8 changes: 3 additions & 5 deletions packages/reactDom/test/test_RSC_html.ml
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,11 @@ let client_with_element_props () =
in
assert_html (app ())
~shell:
"Client with elment prop<script data-payload='0:[\"$\",\"$2\",null,{\"element\":\"$1\"},null,[],{}]\n\
"Client with elment prop<script \
data-payload='0:[\"$\",\"$1\",null,{\"element\":[\"$\",\"span\",null,{\"children\":\"server-component-as-props-to-client-component\"},null,[],{}]},null,[],{}]\n\
'>window.srr_stream.push()</script>"
[
"<script \
data-payload='1:[\"$\",\"span\",null,{\"children\":\"server-component-as-props-to-client-component\"},null,[],{}]\n\
'>window.srr_stream.push()</script>";
"<script data-payload='2:I[\"./client-with-props.js\",[],\"ClientWithProps\"]\n\
"<script data-payload='1:I[\"./client-with-props.js\",[],\"ClientWithProps\"]\n\
'>window.srr_stream.push()</script>";
]

Expand Down
76 changes: 44 additions & 32 deletions packages/reactDom/test/test_RSC_model.ml
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,8 @@ let upper_case_with_list () =
let%lwt () = ReactServerDOM.render_model ~subscribe (app ()) in
assert_list_of_strings !output
[
"1:[\"$\",\"span\",null,{\"children\":\"hi\"},null,[],{}]\n";
"2:[\"$\",\"span\",null,{\"children\":\"hola\"},null,[],{}]\n";
"0:[\"$1\",\"$2\"]\n";
"1:[\"$\",\"span\",null,{\"children\":\"hola\"},null,[],{}]\n";
"0:[[\"$\",\"span\",null,{\"children\":\"hi\"},null,[],{}],\"$1\"]\n";
];
Lwt.return ()

Expand All @@ -169,9 +168,8 @@ let upper_case_with_children () =
let%lwt () = ReactServerDOM.render_model ~subscribe (app ()) in
assert_list_of_strings !output
[
"1:[\"$\",\"span\",null,{\"children\":\"hi\"},null,[],{}]\n";
"2:[\"$\",\"span\",null,{\"children\":\"hola\"},null,[],{}]\n";
"0:[\"$\",\"div\",null,{\"children\":[\"$1\",\"$2\"]},null,[],{}]\n";
"1:[\"$\",\"span\",null,{\"children\":\"hola\"},null,[],{}]\n";
"0:[\"$\",\"div\",null,{\"children\":[[\"$\",\"span\",null,{\"children\":\"hi\"},null,[],{}],\"$1\"]},null,[],{}]\n";
];
Lwt.return ()

Expand All @@ -191,9 +189,8 @@ let suspense_without_promise () =
let%lwt () = ReactServerDOM.render_model ~subscribe main in
assert_list_of_strings !output
[
"1:[\"$\",\"span\",null,{\"children\":\"hi\"},null,[],{}]\n";
"2:[\"$\",\"span\",null,{\"children\":\"hola\"},null,[],{}]\n";
"0:[\"$\",\"$Sreact.suspense\",null,{\"fallback\":\"Loading...\",\"children\":[\"$\",\"div\",null,{\"children\":[\"$1\",\"$2\"]},null,[],{}]},null,[],{}]\n";
"1:[\"$\",\"span\",null,{\"children\":\"hola\"},null,[],{}]\n";
"0:[\"$\",\"$Sreact.suspense\",null,{\"fallback\":\"Loading...\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"span\",null,{\"children\":\"hi\"},null,[],{}],\"$1\"]},null,[],{}]},null,[],{}]\n";
];
Lwt.return ()

Expand Down Expand Up @@ -529,9 +526,8 @@ let client_with_element_props () =
assert_list_of_strings !output
[
"1:I[\"./client-with-props.js\",[],\"ClientWithProps\"]\n";
"2:\"Client Content\"\n";
"0:[[\"$\",\"div\",null,{\"children\":\"Server \
Content\"},null,[],{}],[\"$\",\"$1\",null,{\"children\":\"$2\"},null,[],{}]]\n";
"0:[[\"$\",\"div\",null,{\"children\":\"Server Content\"},null,[],{}],[\"$\",\"$1\",null,{\"children\":\"Client \
Content\"},null,[],{}]]\n";
];
Lwt.return ()

Expand Down Expand Up @@ -658,9 +654,9 @@ let client_with_server_children () =
assert_list_of_strings !output
[
"1:I[\"./client-with-server-children.js\",[],\"ClientWithServerChildren\"]\n";
"2:[\"$\",\"div\",null,{\"children\":\"Server Component Inside Client\"},null,[],{}]\n";
"0:[[\"$\",\"div\",null,{\"children\":\"Server \
Content\"},null,[],{}],[\"$\",\"$1\",null,{\"children\":\"$2\"},null,[],{}]]\n";
Content\"},null,[],{}],[\"$\",\"$1\",null,{\"children\":[\"$\",\"div\",null,{\"children\":\"Server Component \
Inside Client\"},null,[],{}]},null,[],{}]]\n";
];
Lwt.return ()

Expand Down Expand Up @@ -902,26 +898,40 @@ let nested_context () =
(* TODO: Don't push multiple scripts for the same client component *)
assert_list_of_strings !output
[
"1:I[\"./provider.js\",[],\"Provider\"]\n";
"2:I[\"./provider.js\",[],\"Provider\"]\n";
"3:I[\"./provider.js\",[],\"Provider\"]\n";
"4:I[\"./provider.js\",[],\"Provider\"]\n";
"6:I[\"./provider.js\",[],\"Provider\"]\n";
"8:I[\"./provider.js\",[],\"Provider\"]\n";
"9:null\n";
"a:\"Hey you\"\n";
"7:[\"$\",\"$8\",null,{\"value\":\"$9\",\"children\":\"$a\"},null,[],{}]\n";
"b:\"$7\"\n";
"c:I[\"./consumer.js\",[],\"Consumer\"]\n";
"d:[\"/me\",[\"$\",\"$c\",null,{},null,[],{}]]\n";
"5:[\"$\",\"$6\",null,{\"value\":\"$b\",\"children\":\"$d\"},null,[],{}]\n";
"e:\"$5\"\n";
"f:I[\"./consumer.js\",[],\"Consumer\"]\n";
"10:[\"/about\",[\"$\",\"$f\",null,{},null,[],{}]]\n";
"3:[\"$\",\"$4\",null,{\"value\":\"$e\",\"children\":\"$10\"},null,[],{}]\n";
"11:\"$3\"\n";
"12:I[\"./consumer.js\",[],\"Consumer\"]\n";
"13:[\"/root\",[\"$\",\"$12\",null,{},null,[],{}]]\n";
"1:[\"$\",\"$2\",null,{\"value\":\"$11\",\"children\":\"$13\"},null,[],{}]\n";
"0:\"$1\"\n";
"5:I[\"./consumer.js\",[],\"Consumer\"]\n";
"6:I[\"./consumer.js\",[],\"Consumer\"]\n";
"7:I[\"./consumer.js\",[],\"Consumer\"]\n";
"0:[\"$\",\"$1\",null,{\"value\":[\"$\",\"$2\",null,{\"value\":[\"$\",\"$3\",null,{\"value\":[\"$\",\"$4\",null,{\"value\":null,\"children\":\"Hey \
you\"},null,[],{}],\"children\":[\"/me\",[\"$\",\"$5\",null,{},null,[],{}]]},null,[],{}],\"children\":[\"/about\",[\"$\",\"$6\",null,{},null,[],{}]]},null,[],{}],\"children\":[\"/root\",[\"$\",\"$7\",null,{},null,[],{}]]},null,[],{}]\n";
];
Lwt.return ()

let nested_upper_case_component () =
let app () =
React.Upper_case_component ("app", fun () -> React.Upper_case_component ("Foo", fun () -> React.string "Hello"))
in
let output, subscribe = capture_stream () in
let%lwt () = ReactServerDOM.render_model ~subscribe (app ()) in
assert_list_of_strings !output [ "0:\"Hello\"\n" ];
Lwt.return ()

let nested_upper_case_component_debug () =
let app () =
React.Upper_case_component ("app", fun () -> React.Upper_case_component ("Foo", fun () -> React.string "Hello"))
in
let output, subscribe = capture_stream () in
let%lwt () = ReactServerDOM.render_model ~debug:true ~subscribe (app ()) in
assert_list_of_strings !output
[
"1:{\"name\":\"app\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{}}\n";
"0:D\"$1\"\n";
"2:{\"name\":\"Foo\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{}}\n";
"0:D\"$2\"\n";
"0:\"Hello\"\n";
];
Lwt.return ()

Expand Down Expand Up @@ -964,6 +974,8 @@ let tests =
test "client_component_with_resources_metadata" client_component_with_resources_metadata;
test "page_with_hoisted_resources" page_with_hoisted_resources;
test "nested_context" nested_context;
test "nested_upper_case_component" nested_upper_case_component;
test "nested_upper_case_component_debug" nested_upper_case_component_debug;
(* TODO: https://github.com/ml-in-barcelona/server-reason-react/issues/251 test "client_with_promise_failed_props" client_with_promise_failed_props; *)
(* test "env_development_adds_debug_info_2" env_development_adds_debug_info_2; *)
]
Loading