Repository: ml-in-barcelona/server-reason-react Branch: main Commit: c9ab81197425 Files: 794 Total size: 3.9 MB Directory structure: gitextract_rs9v3350/ ├── .dockerignore ├── .gitattributes ├── .githooks/ │ └── pre-push ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── benchmark.yml │ ├── ci.yml │ └── docker.yml ├── .gitignore ├── .ocamlformat ├── CHANGES.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── arch/ │ ├── browser/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── public/ │ │ │ ├── index.html │ │ │ └── manifest.json │ │ └── src/ │ │ ├── index.css │ │ └── index.js │ └── server/ │ ├── head-ordering.js │ ├── package.json │ ├── react-dom-server-node-dom-props.js │ ├── react-dom-server.js │ ├── render-html-to-stream.js │ ├── render-rsc-to-stream.js │ ├── test-useid-edge-cases.js │ └── test-useid.js ├── benchmark/ │ ├── Makefile │ ├── README.md │ ├── allocation.ml │ ├── bench.ml │ ├── dune │ ├── frameworks/ │ │ ├── bun-native/ │ │ │ └── server.tsx │ │ ├── hono-bun/ │ │ │ └── server.ts │ │ ├── hono-node/ │ │ │ └── server.mjs │ │ ├── node-express/ │ │ │ └── server.mjs │ │ ├── node-fastify/ │ │ │ └── server.mjs │ │ ├── package.json │ │ ├── preact/ │ │ │ └── server.mjs │ │ ├── render-bench.ts │ │ └── shared/ │ │ ├── Blog.jsx │ │ ├── Dashboard.jsx │ │ ├── Ecommerce.jsx │ │ ├── Form.jsx │ │ ├── PropsHeavy.jsx │ │ ├── cx.js │ │ └── scenarios.jsx │ ├── memory/ │ │ ├── dune │ │ └── memory_bench.ml │ ├── native/ │ │ ├── dune │ │ └── server.re │ ├── perf-work/ │ │ ├── PERF_NEXT.md │ │ ├── README.md │ │ ├── alloc-table500.txt │ │ ├── alloc-wide500.txt │ │ ├── alloc_profile.ml │ │ ├── baseline-run1.txt │ │ ├── baseline-run2.txt │ │ ├── baseline-run3.txt │ │ ├── cycles-out/ │ │ │ ├── callgrind-table100.annotate.stderr │ │ │ ├── callgrind-table100.out │ │ │ ├── callgrind-table100.stderr │ │ │ ├── callgrind-table100.stdout │ │ │ ├── callgrind-table100.txt │ │ │ ├── callgrind-wide100.annotate.stderr │ │ │ ├── callgrind-wide100.out │ │ │ ├── callgrind-wide100.stderr │ │ │ ├── callgrind-wide100.stdout │ │ │ ├── callgrind-wide100.txt │ │ │ ├── perf-stat-wide100.stdout │ │ │ └── perf-stat-wide100.txt │ │ ├── diff_styles.ml │ │ ├── dump_html.ml │ │ ├── dune │ │ ├── dune_extra │ │ ├── perf_profile.ml │ │ ├── perf_profile.sh │ │ ├── phase1-run1.txt │ │ ├── phase1-run2.txt │ │ ├── phase2-final.txt │ │ ├── phase2-run1.txt │ │ ├── phase2-run2.txt │ │ ├── phase3-run1.txt │ │ ├── phase3-run2.txt │ │ ├── phase3b-run1.txt │ │ ├── phase3b-run2.txt │ │ ├── phase3c-run1.txt │ │ ├── phase3c-run2.txt │ │ ├── phase4-final.txt │ │ ├── phase4-run1.txt │ │ ├── phase7-final.txt │ │ ├── style_alloc_bench.ml │ │ ├── unification_bench.ml │ │ ├── unified-experiment-runs.txt │ │ └── unified-experiment.txt │ ├── results/ │ │ └── .gitkeep │ ├── runner/ │ │ ├── package.json │ │ ├── runner.mjs │ │ └── visualize.html │ ├── scenarios/ │ │ ├── Blog.re │ │ ├── Cx.re │ │ ├── Dashboard.re │ │ ├── DeepTree.re │ │ ├── Ecommerce.re │ │ ├── Form.re │ │ ├── PropsHeavy.re │ │ ├── ShallowTree.re │ │ ├── Table.re │ │ ├── Trivial.re │ │ ├── WideTree.re │ │ └── dune │ └── streaming/ │ ├── dune │ └── streaming_bench.ml ├── demo/ │ ├── README.md │ ├── client/ │ │ ├── DummyRouterRSC.re │ │ ├── HydrateRoot.re │ │ ├── NestedRouterRSC.re │ │ ├── RenderRoot.re │ │ ├── ServerOnlyRSC.re │ │ ├── SinglePageRSC.re │ │ ├── build.mjs │ │ ├── dune │ │ └── package.json │ ├── dream-nested-router/ │ │ ├── dune │ │ ├── js/ │ │ │ ├── HistoryCache.re │ │ │ ├── HistoryState.re │ │ │ ├── VirtualHistory.re │ │ │ └── dune │ │ ├── native/ │ │ │ ├── README.md │ │ │ ├── RouterRSC.re │ │ │ ├── RouterRSC.rei │ │ │ ├── dune │ │ │ └── shared/ │ │ │ ├── DynamicParams.re │ │ │ ├── NavigationResponse.re │ │ │ ├── Route.re │ │ │ ├── Router.re │ │ │ └── Router.rei │ │ └── test_router_rsc.ml │ ├── dream-rsc/ │ │ ├── DreamRSC.re │ │ ├── DreamRSC.rei │ │ └── dune │ ├── dune │ ├── package.json │ ├── server/ │ │ ├── db/ │ │ │ └── notes.json │ │ ├── dune │ │ ├── pages/ │ │ │ ├── Comments.re │ │ │ ├── DummyRouterRSC.re │ │ │ ├── Home.re │ │ │ ├── NestedRouter.re │ │ │ ├── NoteItem.re │ │ │ ├── NoteList.re │ │ │ ├── ServerOnlyRSC.re │ │ │ ├── SidebarNote.re │ │ │ └── SinglePageRSC.re │ │ └── server.re │ ├── styles.css │ ├── tailwind.config.js │ └── universal/ │ ├── js/ │ │ ├── Dream.re │ │ └── dune │ └── native/ │ ├── DB.re │ ├── Date.re │ ├── FunctionReferences.re │ ├── FunctionReferences.rei │ ├── Markdown.re │ ├── SidebarNote.re │ ├── dune │ └── shared/ │ ├── Align.re │ ├── App.re │ ├── Arrow.re │ ├── Button.re │ ├── Context.re │ ├── Counter.re │ ├── Cx.re │ ├── Debug_props.re │ ├── DeleteNoteButton.re │ ├── DemoLayout.re │ ├── Document.re │ ├── DummyClientRouter.re │ ├── Expander.re │ ├── GlobalStyles.re │ ├── Hr.re │ ├── InputText.re │ ├── Link.re │ ├── NestedRouter_CreateNoteButton.re │ ├── NestedRouter_DeleteNoteButton.re │ ├── NestedRouter_EditButton.re │ ├── NestedRouter_NoteEditor.re │ ├── NestedRouter_NoteItem.re │ ├── NestedRouter_NoteList.re │ ├── NestedRouter_SearchField.re │ ├── NestedRouter_SidebarNote.re │ ├── NestedRouter_SidebarNoteContent.re │ ├── Note.re │ ├── NoteEditor.re │ ├── NoteListSkeleton.re │ ├── NotePreview.re │ ├── NoteSkeleton.re │ ├── Promise_renderer.re │ ├── RR.re │ ├── RequestContextDemo.re │ ├── Routes.re │ ├── Row.re │ ├── SearchField.re │ ├── ServerActionFromPropsClient.re │ ├── ServerActionWithError.re │ ├── ServerActionWithFormData.re │ ├── ServerActionWithFormDataFormAction.re │ ├── ServerActionWithFormDataServer.re │ ├── ServerActionWithFormDataWithArg.re │ ├── ServerActionWithOptionalArg.re │ ├── ServerActionWithSimpleResponse.re │ ├── ServerFunctions.re │ ├── SidebarNoteContent.re │ ├── Spinner.re │ ├── Stack.re │ ├── Static_small.re │ ├── Text.re │ ├── Textarea.re │ └── Theme.re ├── documentation/ │ ├── browser_ppx.mld │ ├── dune │ ├── externals-melange-attributes.mld │ ├── get-started.mld │ ├── how-to-organise-universal-code.mld │ ├── index.mld │ ├── ssr-and-hydration.mld │ └── universal-code.mld ├── dune ├── dune-project ├── fly.toml ├── packages/ │ ├── Belt/ │ │ ├── src/ │ │ │ ├── Belt.re │ │ │ ├── Belt_Array.ml │ │ │ ├── Belt_Array.mli │ │ │ ├── Belt_Float.ml │ │ │ ├── Belt_Float.mli │ │ │ ├── Belt_HashMap.ml │ │ │ ├── Belt_HashMap.mli │ │ │ ├── Belt_HashMapInt.ml │ │ │ ├── Belt_HashMapInt.mli │ │ │ ├── Belt_HashMapString.ml │ │ │ ├── Belt_HashMapString.mli │ │ │ ├── Belt_HashSet.ml │ │ │ ├── Belt_HashSet.mli │ │ │ ├── Belt_HashSetInt.ml │ │ │ ├── Belt_HashSetInt.mli │ │ │ ├── Belt_HashSetString.ml │ │ │ ├── Belt_HashSetString.mli │ │ │ ├── Belt_Id.ml │ │ │ ├── Belt_Id.mli │ │ │ ├── Belt_Int.ml │ │ │ ├── Belt_Int.mli │ │ │ ├── Belt_List.ml │ │ │ ├── Belt_List.mli │ │ │ ├── Belt_Map.ml │ │ │ ├── Belt_Map.mli │ │ │ ├── Belt_MapDict.ml │ │ │ ├── Belt_MapDict.mli │ │ │ ├── Belt_MapInt.ml │ │ │ ├── Belt_MapInt.mli │ │ │ ├── Belt_MapString.ml │ │ │ ├── Belt_MapString.mli │ │ │ ├── Belt_MutableMap.ml │ │ │ ├── Belt_MutableMap.mli │ │ │ ├── Belt_MutableMapInt.ml │ │ │ ├── Belt_MutableMapInt.mli │ │ │ ├── Belt_MutableMapString.ml │ │ │ ├── Belt_MutableMapString.mli │ │ │ ├── Belt_MutableQueue.ml │ │ │ ├── Belt_MutableQueue.mli │ │ │ ├── Belt_MutableSet.ml │ │ │ ├── Belt_MutableSet.mli │ │ │ ├── Belt_MutableSetInt.ml │ │ │ ├── Belt_MutableSetInt.mli │ │ │ ├── Belt_MutableSetString.ml │ │ │ ├── Belt_MutableSetString.mli │ │ │ ├── Belt_MutableStack.ml │ │ │ ├── Belt_MutableStack.mli │ │ │ ├── Belt_Option.ml │ │ │ ├── Belt_Option.mli │ │ │ ├── Belt_Range.ml │ │ │ ├── Belt_Range.mli │ │ │ ├── Belt_Result.ml │ │ │ ├── Belt_Result.mli │ │ │ ├── Belt_Set.ml │ │ │ ├── Belt_Set.mli │ │ │ ├── Belt_SetDict.ml │ │ │ ├── Belt_SetDict.mli │ │ │ ├── Belt_SetInt.ml │ │ │ ├── Belt_SetInt.mli │ │ │ ├── Belt_SetString.ml │ │ │ ├── Belt_SetString.mli │ │ │ ├── Belt_SortArray.ml │ │ │ ├── Belt_SortArray.mli │ │ │ ├── Belt_SortArrayInt.ml │ │ │ ├── Belt_SortArrayInt.mli │ │ │ ├── Belt_SortArrayString.ml │ │ │ ├── Belt_SortArrayString.mli │ │ │ ├── Belt_internalAVLset.ml │ │ │ ├── Belt_internalAVLset.mli │ │ │ ├── Belt_internalAVLtree.ml │ │ │ ├── Belt_internalAVLtree.mli │ │ │ ├── Belt_internalBuckets.ml │ │ │ ├── Belt_internalBuckets.mli │ │ │ ├── Belt_internalBucketsType.ml │ │ │ ├── Belt_internalBucketsType.mli │ │ │ ├── Belt_internalMapInt.ml │ │ │ ├── Belt_internalMapString.ml │ │ │ ├── Belt_internalSetBuckets.ml │ │ │ ├── Belt_internalSetBuckets.mli │ │ │ ├── Belt_internalSetInt.ml │ │ │ ├── Belt_internalSetString.ml │ │ │ ├── caml_hash.ml │ │ │ ├── dune │ │ │ └── stubs.c │ │ └── test/ │ │ ├── Test_Belt_Array.ml │ │ ├── Test_Belt_Float.ml │ │ ├── Test_Belt_HashMap.ml │ │ ├── Test_Belt_HashMap_Int.ml │ │ ├── Test_Belt_HashMap_String.ml │ │ ├── Test_Belt_HashSet_Int.ml │ │ ├── Test_Belt_HashSet_String.ml │ │ ├── Test_Belt_Int.ml │ │ ├── Test_Belt_List.ml │ │ ├── Test_Belt_Map.ml │ │ ├── Test_Belt_Map_Dict.ml │ │ ├── Test_Belt_Map_Int.ml │ │ ├── Test_Belt_Map_String.ml │ │ ├── Test_Belt_MutableMap.ml │ │ ├── Test_Belt_MutableMap_Int.ml │ │ ├── Test_Belt_MutableMap_String.ml │ │ ├── Test_Belt_MutableQueue.ml │ │ ├── Test_Belt_MutableSet.ml │ │ ├── Test_Belt_MutableSet_Int.ml │ │ ├── Test_Belt_MutableSet_String.ml │ │ ├── Test_Belt_MutableStack.ml │ │ ├── Test_Belt_Option.ml │ │ ├── Test_Belt_Result.ml │ │ ├── Test_Belt_Set.ml │ │ ├── Test_Belt_Set_Dict.ml │ │ ├── Test_Belt_Set_Int.ml │ │ ├── Test_Belt_Set_String.ml │ │ ├── Test_Belt_SortArray.ml │ │ ├── Test_Belt_SortArray_Int.ml │ │ ├── Test_Belt_SortArray_String.ml │ │ ├── Test_Belt_Support.ml │ │ ├── benchmark.ml │ │ ├── dune │ │ └── test.ml │ ├── Dom/ │ │ ├── Dom.ml │ │ ├── Dom_storage.ml │ │ └── dune │ ├── Js/ │ │ ├── lib/ │ │ │ ├── Js.ml │ │ │ ├── Js.mli │ │ │ ├── Js_array.ml │ │ │ ├── Js_array.mli │ │ │ ├── Js_bigint.ml │ │ │ ├── Js_bigint.mli │ │ │ ├── Js_console.ml │ │ │ ├── Js_console.mli │ │ │ ├── Js_date.ml │ │ │ ├── Js_date.mli │ │ │ ├── Js_dict.ml │ │ │ ├── Js_dict.mli │ │ │ ├── Js_exn.ml │ │ │ ├── Js_exn.mli │ │ │ ├── Js_float.ml │ │ │ ├── Js_float.mli │ │ │ ├── Js_formdata.ml │ │ │ ├── Js_formdata.mli │ │ │ ├── Js_global.ml │ │ │ ├── Js_global.mli │ │ │ ├── Js_int.ml │ │ │ ├── Js_int.mli │ │ │ ├── Js_internal.ml │ │ │ ├── Js_internal.mli │ │ │ ├── Js_json.ml │ │ │ ├── Js_json.mli │ │ │ ├── Js_map.ml │ │ │ ├── Js_map.mli │ │ │ ├── Js_math.ml │ │ │ ├── Js_math.mli │ │ │ ├── Js_null.ml │ │ │ ├── Js_null.mli │ │ │ ├── Js_nullable.ml │ │ │ ├── Js_nullable.mli │ │ │ ├── Js_obj.ml │ │ │ ├── Js_obj.mli │ │ │ ├── Js_promise.ml │ │ │ ├── Js_promise.mli │ │ │ ├── Js_re.ml │ │ │ ├── Js_re.mli │ │ │ ├── Js_set.ml │ │ │ ├── Js_set.mli │ │ │ ├── Js_string.ml │ │ │ ├── Js_string.mli │ │ │ ├── Js_typed_array.ml │ │ │ ├── Js_typed_array.mli │ │ │ ├── Js_typed_array2.ml │ │ │ ├── Js_typed_array2.mli │ │ │ ├── Js_types.ml │ │ │ ├── Js_types.mli │ │ │ ├── Js_undefined.ml │ │ │ ├── Js_undefined.mli │ │ │ ├── Js_vector.ml │ │ │ ├── Js_vector.mli │ │ │ ├── Js_weakmap.ml │ │ │ ├── Js_weakmap.mli │ │ │ ├── Js_weakset.ml │ │ │ ├── Js_weakset.mli │ │ │ └── dune │ │ └── test/ │ │ ├── bigint_tests/ │ │ │ ├── arithmetic.ml │ │ │ ├── as_int_n.ml │ │ │ ├── as_uint_n.ml │ │ │ ├── bitwise.ml │ │ │ ├── comparison.ml │ │ │ ├── constructor.ml │ │ │ ├── conversion.ml │ │ │ └── prototype.ml │ │ ├── date_tests/ │ │ │ ├── getters.ml │ │ │ ├── local_getters.ml │ │ │ ├── now.ml │ │ │ ├── parse.ml │ │ │ ├── setters.ml │ │ │ ├── to_iso_string.ml │ │ │ ├── to_string.ml │ │ │ └── utc.ml │ │ ├── dune │ │ ├── helpers.ml │ │ ├── number_tests/ │ │ │ ├── is_finite.ml │ │ │ ├── is_integer.ml │ │ │ ├── is_nan.ml │ │ │ ├── parse_float.ml │ │ │ ├── parse_int.ml │ │ │ ├── to_exponential.ml │ │ │ ├── to_precision.ml │ │ │ └── to_string.ml │ │ ├── regexp_tests/ │ │ │ ├── dotall.ml │ │ │ ├── named_groups.ml │ │ │ └── unicode.ml │ │ ├── string_tests/ │ │ │ ├── normalize.ml │ │ │ └── search.ml │ │ ├── test.ml │ │ └── undefined_tests/ │ │ └── undefined.ml │ ├── browser-ppx/ │ │ ├── dune │ │ ├── ppx.ml │ │ └── tests/ │ │ ├── at_browser_only.t │ │ ├── at_platform.t │ │ ├── dune │ │ ├── pexp_apply.t │ │ ├── pexp_constraint_re.t │ │ ├── pexp_fun.t │ │ ├── pexp_fun_with_vb.t │ │ ├── pexp_function.t │ │ ├── pexp_ident.t │ │ ├── playground.t/ │ │ │ ├── input.re │ │ │ └── run.t │ │ ├── preprocess.t │ │ ├── standalone.ml │ │ ├── structure_item.t │ │ ├── structure_item_re.t │ │ ├── switch-platform.t/ │ │ │ ├── input.re │ │ │ └── run.t │ │ └── use_effect.t │ ├── esbuild-plugin/ │ │ ├── dune │ │ ├── extract_client_components.ml │ │ ├── package.json │ │ ├── plugin.mjs │ │ └── test/ │ │ ├── ClientComponent.js │ │ ├── ClientComponentWithModule.js │ │ ├── ServerFunction.js │ │ ├── dune │ │ └── run.t │ ├── expand-styles-attribute/ │ │ ├── dune │ │ ├── expand_styles_attribute.ml │ │ └── test/ │ │ ├── dune │ │ └── test.ml │ ├── fetch/ │ │ ├── Fetch.ml │ │ └── dune │ ├── html/ │ │ ├── Html.ml │ │ └── dune │ ├── melange.ppx/ │ │ ├── base32/ │ │ │ ├── LICENSES/ │ │ │ │ └── ISC.txt │ │ │ ├── README.md │ │ │ └── lib/ │ │ │ ├── base32.ml │ │ │ ├── base32.mli │ │ │ └── dune │ │ ├── derive_util.ml │ │ ├── double_hash.ml │ │ ├── dune │ │ ├── get_set.ml │ │ ├── js_converter.ml │ │ ├── js_properties.ml │ │ ├── pipe_first.ml │ │ ├── ppx.ml │ │ ├── regex.ml │ │ ├── tests/ │ │ │ ├── dune │ │ │ ├── external.t │ │ │ ├── input.ml │ │ │ ├── jsConverter.t │ │ │ ├── jsProperties.t │ │ │ ├── mel_as.t │ │ │ ├── mel_module.t │ │ │ ├── mel_obj.t │ │ │ ├── mel_raw.t │ │ │ ├── mel_send.t │ │ │ ├── mel_send_pipe.t │ │ │ ├── pipe_first.t/ │ │ │ │ ├── input.ml │ │ │ │ └── run.t │ │ │ ├── private.t │ │ │ ├── regex.t/ │ │ │ │ ├── input.ml │ │ │ │ └── run.t │ │ │ ├── standalone.ml │ │ │ └── string_interpolation.t │ │ └── xxhash/ │ │ ├── XXH64.ml │ │ ├── dune │ │ └── test_xxh64.ml │ ├── promise/ │ │ ├── js/ │ │ │ ├── dune │ │ │ ├── promise.re │ │ │ └── promise.rei │ │ └── native/ │ │ ├── dune │ │ ├── promise.re │ │ └── promise.rei │ ├── react/ │ │ ├── src/ │ │ │ ├── React.ml │ │ │ ├── React.mli │ │ │ ├── ReactEvent.ml │ │ │ ├── ReasonReactRouter.ml │ │ │ ├── ReasonReactRouter.mli │ │ │ └── dune │ │ └── test/ │ │ ├── dune │ │ ├── test.ml │ │ ├── test_cloneElement.ml │ │ └── test_react.ml │ ├── react-server-dom-esbuild/ │ │ ├── ReactServerDOMEsbuild.js │ │ ├── ReactServerDOMEsbuild.re │ │ ├── dune │ │ └── package.json │ ├── reactDom/ │ │ ├── src/ │ │ │ ├── Push_stream.ml │ │ │ ├── ReactDOM.ml │ │ │ ├── ReactDOM.mli │ │ │ ├── ReactDOMStyle.ml │ │ │ ├── ReactDOMStyle.mli │ │ │ ├── ReactServerDOM.ml │ │ │ ├── ReactServerDOM.mli │ │ │ └── dune │ │ └── test/ │ │ ├── dune │ │ ├── test.ml │ │ ├── test_RSC_decoders.ml │ │ ├── test_RSC_html.ml │ │ ├── test_RSC_html_shell.ml │ │ ├── test_RSC_model.ml │ │ ├── test_reactDOMStyle.ml │ │ ├── test_renderToStaticMarkup.ml │ │ ├── test_renderToStream.ml │ │ ├── test_renderToString.ml │ │ ├── test_useId.ml │ │ └── test_write_to_buffer.ml │ ├── rsc/ │ │ ├── README.md │ │ ├── js/ │ │ │ ├── RSC.ml │ │ │ ├── RSC.mli │ │ │ └── dune │ │ ├── native/ │ │ │ ├── RSC.ml │ │ │ ├── RSC.mli │ │ │ └── dune │ │ ├── ppx_common/ │ │ │ ├── dune │ │ │ ├── ppx_deriving_tools.ml │ │ │ ├── ppx_deriving_tools.mli │ │ │ └── rsc_deriving_common.ml │ │ ├── ppx_js/ │ │ │ ├── dune │ │ │ └── ppx_deriving_rsc_js.ml │ │ └── ppx_native/ │ │ ├── dune │ │ └── ppx_deriving_rsc_native.ml │ ├── runtime/ │ │ ├── Runtime.ml │ │ ├── Runtime.mli │ │ └── dune │ ├── server-reason-react-ppx/ │ │ ├── DomProps.ml │ │ ├── DomProps.mli │ │ ├── Style_rewrite.ml │ │ ├── cram/ │ │ │ ├── client-component-e2e.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── client-component-no-props.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── client-component-on-the-client-nested.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── client-component-on-the-client.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── client-component-on-the-server.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── client-component-with-fn-error.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── client-props-decoding.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── component-definition-at-toplevel.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── component-definition.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── component-defintion-signatures.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── dune │ │ │ ├── dune-describe-pp.sh │ │ │ ├── ensure-attributes-are-present.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── external.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── functor.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── jsx-fragment.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── locations/ │ │ │ │ ├── input.re │ │ │ │ └── run │ │ │ ├── lower-call-missing-prop.t/ │ │ │ │ ├── input.re │ │ │ │ ├── run.t │ │ │ │ └── wrong-prop.re │ │ │ ├── lower-call-reserved-prop.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── lower-calls.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── ppx.sh │ │ │ ├── reason.expected │ │ │ ├── server-client-props.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── server-function-on-client.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── server-function-on-server.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── shared-folder-prefix-melange.t/ │ │ │ │ ├── js/ │ │ │ │ │ └── input.re │ │ │ │ └── run.t │ │ │ ├── shared-folder-prefix-native.t/ │ │ │ │ ├── native/ │ │ │ │ │ └── input.ml │ │ │ │ └── run.t │ │ │ ├── standalone.ml │ │ │ ├── styles.t/ │ │ │ │ ├── input.re │ │ │ │ └── run.t │ │ │ ├── temp.ml │ │ │ ├── upper-calls-ocaml.t/ │ │ │ │ ├── input.ml │ │ │ │ └── run.t │ │ │ └── upper-calls.t/ │ │ │ ├── input.re │ │ │ └── run.t │ │ ├── dune │ │ ├── server_reason_react_ppx.ml │ │ ├── static_analysis.ml │ │ └── test/ │ │ ├── dune │ │ └── test.re │ ├── url/ │ │ ├── URL.rei │ │ ├── js/ │ │ │ ├── URL.re │ │ │ └── dune │ │ ├── native/ │ │ │ ├── URL.re │ │ │ └── dune │ │ └── test/ │ │ ├── dune │ │ └── test_native.re │ └── webapi/ │ ├── src/ │ │ ├── Canvas/ │ │ │ ├── Webapi__Canvas__Canvas2d.re │ │ │ └── Webapi__Canvas__WebGl.re │ │ ├── Dom/ │ │ │ ├── Webapi__Dom__AnimationEvent.re │ │ │ ├── Webapi__Dom__Attr.re │ │ │ ├── Webapi__Dom__BeforeUnloadEvent.re │ │ │ ├── Webapi__Dom__CdataSection.re │ │ │ ├── Webapi__Dom__CharacterData.re │ │ │ ├── Webapi__Dom__ChildNode.re │ │ │ ├── Webapi__Dom__ClipboardEvent.re │ │ │ ├── Webapi__Dom__CloseEvent.re │ │ │ ├── Webapi__Dom__Comment.re │ │ │ ├── Webapi__Dom__CompositionEvent.re │ │ │ ├── Webapi__Dom__CssStyleDeclaration.re │ │ │ ├── Webapi__Dom__CustomEvent.re │ │ │ ├── Webapi__Dom__Document.re │ │ │ ├── Webapi__Dom__DocumentFragment.re │ │ │ ├── Webapi__Dom__DocumentOrShadowRoot.re │ │ │ ├── Webapi__Dom__DocumentType.re │ │ │ ├── Webapi__Dom__DomImplementation.re │ │ │ ├── Webapi__Dom__DomRect.re │ │ │ ├── Webapi__Dom__DomStringMap.re │ │ │ ├── Webapi__Dom__DomTokenList.re │ │ │ ├── Webapi__Dom__DragEvent.re │ │ │ ├── Webapi__Dom__Element.re │ │ │ ├── Webapi__Dom__ErrorEvent.re │ │ │ ├── Webapi__Dom__Event.re │ │ │ ├── Webapi__Dom__EventTarget.re │ │ │ ├── Webapi__Dom__FocusEvent.re │ │ │ ├── Webapi__Dom__GlobalEventHandlers.re │ │ │ ├── Webapi__Dom__History.re │ │ │ ├── Webapi__Dom__HtmlCollection.re │ │ │ ├── Webapi__Dom__HtmlDocument.re │ │ │ ├── Webapi__Dom__HtmlElement.re │ │ │ ├── Webapi__Dom__HtmlFormElement.re │ │ │ ├── Webapi__Dom__HtmlImageElement.re │ │ │ ├── Webapi__Dom__HtmlInputElement.re │ │ │ ├── Webapi__Dom__IdbVersionChangeEvent.re │ │ │ ├── Webapi__Dom__Image.re │ │ │ ├── Webapi__Dom__InputEvent.re │ │ │ ├── Webapi__Dom__KeyboardEvent.re │ │ │ ├── Webapi__Dom__Location.re │ │ │ ├── Webapi__Dom__MouseEvent.re │ │ │ ├── Webapi__Dom__MutationObserver.re │ │ │ ├── Webapi__Dom__MutationRecord.re │ │ │ ├── Webapi__Dom__NamedNodeMap.re │ │ │ ├── Webapi__Dom__Node.re │ │ │ ├── Webapi__Dom__NodeFilter.re │ │ │ ├── Webapi__Dom__NodeIterator.re │ │ │ ├── Webapi__Dom__NodeList.re │ │ │ ├── Webapi__Dom__NonDocumentTypeChildNode.re │ │ │ ├── Webapi__Dom__NonElementParentNode.re │ │ │ ├── Webapi__Dom__PageTransitionEvent.re │ │ │ ├── Webapi__Dom__ParentNode.re │ │ │ ├── Webapi__Dom__PointerEvent.re │ │ │ ├── Webapi__Dom__PopStateEvent.re │ │ │ ├── Webapi__Dom__ProcessingInstruction.re │ │ │ ├── Webapi__Dom__ProgressEvent.re │ │ │ ├── Webapi__Dom__Range.re │ │ │ ├── Webapi__Dom__RelatedEvent.re │ │ │ ├── Webapi__Dom__Selection.re │ │ │ ├── Webapi__Dom__ShadowRoot.re │ │ │ ├── Webapi__Dom__Slotable.re │ │ │ ├── Webapi__Dom__StorageEvent.re │ │ │ ├── Webapi__Dom__SvgZoomEvent.re │ │ │ ├── Webapi__Dom__Text.re │ │ │ ├── Webapi__Dom__TimeEvent.re │ │ │ ├── Webapi__Dom__TouchEvent.re │ │ │ ├── Webapi__Dom__TrackEvent.re │ │ │ ├── Webapi__Dom__TransitionEvent.re │ │ │ ├── Webapi__Dom__TreeWalker.re │ │ │ ├── Webapi__Dom__Types.re │ │ │ ├── Webapi__Dom__UiEvent.re │ │ │ ├── Webapi__Dom__ValidityState.re │ │ │ ├── Webapi__Dom__WebGlContextEvent.re │ │ │ ├── Webapi__Dom__WheelEvent.re │ │ │ └── Webapi__Dom__Window.re │ │ ├── ResizeObserver/ │ │ │ └── Webapi__ResizeObserver__ResizeObserverEntry.re │ │ ├── Webapi.re │ │ ├── Webapi__Base64.re │ │ ├── Webapi__Blob.re │ │ ├── Webapi__Canvas.re │ │ ├── Webapi__Dom.re │ │ ├── Webapi__File.re │ │ ├── Webapi__Performance.re │ │ ├── Webapi__ReadableStream.re │ │ ├── Webapi__ResizeObserver.re │ │ ├── Webapi__Url.re │ │ └── dune │ └── tests/ │ ├── Canvas/ │ │ └── Webapi__Canvas__Canvas2d__test.re │ ├── Dom/ │ │ ├── Webapi__Dom__AnimationEvent__test.re │ │ ├── Webapi__Dom__BeforeUnloadEvent__test.re │ │ ├── Webapi__Dom__ClipboardEvent__test.re │ │ ├── Webapi__Dom__CloseEvent__test.re │ │ ├── Webapi__Dom__CompositionEvent__test.re │ │ ├── Webapi__Dom__CustomEvent__test.re │ │ ├── Webapi__Dom__Document__test.re │ │ ├── Webapi__Dom__DomStringMap__test.re │ │ ├── Webapi__Dom__DomTokenList__test.re │ │ ├── Webapi__Dom__DragEvent__test.re │ │ ├── Webapi__Dom__Element__test.re │ │ ├── Webapi__Dom__ErrorEvent__test.re │ │ ├── Webapi__Dom__EventTarget__test.re │ │ ├── Webapi__Dom__Event__test.re │ │ ├── Webapi__Dom__FocusEvent__test.re │ │ ├── Webapi__Dom__GlobalEventHandlers__test.re │ │ ├── Webapi__Dom__History__test.re │ │ ├── Webapi__Dom__HtmlDocument__test.re │ │ ├── Webapi__Dom__HtmlElement__test.re │ │ ├── Webapi__Dom__HtmlFormElement__test.re │ │ ├── Webapi__Dom__IdbVersionChangeEvent__test.re │ │ ├── Webapi__Dom__Image__test.re │ │ ├── Webapi__Dom__InputEvent__test.re │ │ ├── Webapi__Dom__KeyboardEvent__test.re │ │ ├── Webapi__Dom__Location__test.re │ │ ├── Webapi__Dom__MouseEvent__test.re │ │ ├── Webapi__Dom__NodeList__test.re │ │ ├── Webapi__Dom__Node__test.re │ │ ├── Webapi__Dom__PageTransitionEvent__test.re │ │ ├── Webapi__Dom__PointerEvent__test.re │ │ ├── Webapi__Dom__PopStateEvent__test.re │ │ ├── Webapi__Dom__ProgressEvent__test.re │ │ ├── Webapi__Dom__Range__test.re │ │ ├── Webapi__Dom__RelatedEvent__test.re │ │ ├── Webapi__Dom__Selection__test.re │ │ ├── Webapi__Dom__StorageEvent__test.re │ │ ├── Webapi__Dom__SvgZoomEvent__test.re │ │ ├── Webapi__Dom__Text__test.re │ │ ├── Webapi__Dom__TimeEvent__test.re │ │ ├── Webapi__Dom__TouchEvent__test.re │ │ ├── Webapi__Dom__TrackEvent__test.re │ │ ├── Webapi__Dom__TransitionEvent__test.re │ │ ├── Webapi__Dom__UiEvent__test.re │ │ ├── Webapi__Dom__WebGlContextEvent__test.re │ │ ├── Webapi__Dom__WheelEvent__test.re │ │ └── Webapi__Dom__Window__test.re │ ├── Webapi__Base64__test.re │ ├── Webapi__Blob__test.re │ ├── Webapi__File__test.re │ ├── Webapi__Performace__test.re │ ├── Webapi__ReadableStream__test.re │ ├── Webapi__ResizeObserver__test.re │ ├── Webapi__Url__test.re │ ├── _dune │ └── testHelpers.re ├── server-reason-react.opam └── server-reason-react.opam.template ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ **/_build **/_opam **/_opam_* _opam* node_modules/ **/node_modules/ compare/ **/.merlin *.install .git/ README.md CHANGELOG.md benchmark/ arch/ .github/ ================================================ FILE: .gitattributes ================================================ *.re linguist-language=Reason *.rei linguist-language=Reason *.ml linguist-language=OCaml *.mli linguist-language=OCaml *.mll linguist-language=OCaml *.mly linguist-language=OCaml ================================================ FILE: .githooks/pre-push ================================================ #!/bin/bash if ! ( make format-check ); then echo "some files are not properly formatted, refusing to push" exit 1 fi ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: davesnx jchavarri ================================================ FILE: .github/workflows/benchmark.yml ================================================ name: Framework Comparison on: push: tags: - 'v*' workflow_dispatch: inputs: frameworks: description: 'Frameworks to test (comma-separated, or "all")' required: false default: 'all' scenarios: description: 'Scenarios to run (comma-separated, or "all")' required: false default: 'trivial,table100,table500' jobs: benchmark-frameworks: name: Compare Frameworks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup OCaml uses: ocaml/setup-ocaml@v3 with: ocaml-compiler: 5.2.x dune-cache: true - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Setup Bun uses: oven-sh/setup-bun@v1 - name: Install wrk run: | sudo apt-get update sudo apt-get install -y wrk - name: Install OCaml dependencies run: opam install . --deps-only -y - name: Build native server run: opam exec -- dune build benchmark/native/server.exe --profile=release - name: Install JS framework dependencies working-directory: benchmark/frameworks run: npm install - name: Install runner dependencies working-directory: benchmark/runner run: npm install - name: Start native server run: | opam exec -- _build/default/benchmark/native/server.exe & sleep 2 - name: Run framework comparison working-directory: benchmark/runner run: | FRAMEWORKS="${{ github.event.inputs.frameworks || 'all' }}" SCENARIOS="${{ github.event.inputs.scenarios || 'trivial,table100,table500' }}" ARGS="" if [ "$FRAMEWORKS" != "all" ]; then ARGS="$ARGS --frameworks $FRAMEWORKS" fi if [ "$SCENARIOS" != "all" ]; then ARGS="$ARGS --scenarios $SCENARIOS" fi node runner.mjs $ARGS - name: Upload results uses: actions/upload-artifact@v4 with: name: framework-comparison-${{ github.sha }} path: benchmark/runner/results/ retention-days: 90 - name: Add results to summary run: | echo "## Framework Comparison Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY LATEST=$(ls -t benchmark/runner/results/*.md 2>/dev/null | head -1) if [ -n "$LATEST" ]; then cat "$LATEST" >> $GITHUB_STEP_SUMMARY else echo "No results generated" >> $GITHUB_STEP_SUMMARY fi ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main tags: - '*' pull_request: branches: - main env: DUNE_PROFILE: release OCAMLRUNPARAM: b permissions: contents: write concurrency: group: ci-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} defaults: run: shell: bash -xeuo pipefail {0} jobs: build: name: Build and test runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - macos-latest - ubuntu-latest # - windows-latest ocaml-compiler: - 4.14.1 - 5.4.0 steps: - uses: actions/checkout@v4 - name: Use OCaml ${{ matrix.ocaml-compiler }} uses: ocaml/setup-ocaml@v3.4.5 with: ocaml-compiler: ${{ matrix.ocaml-compiler }} dune-cache: false opam-disable-sandboxing: true - name: Use Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install opam deps run: make install - name: Install npm deps run: make install-npm - name: Pin dependencies run: make pin - name: Build run: make build - name: Check formatting if: matrix.ocaml-compiler != '4.14.1' run: make format-check - name: Run tests run: make test - name: Generate docs if: github.ref == 'refs/heads/main' && matrix.os == 'ubuntu-latest' && matrix.ocaml-compiler == '5.4.0' run: | opam install -y odoc-driver make docs - name: Upload docs artifact if: github.ref == 'refs/heads/main' && matrix.os == 'ubuntu-latest' && matrix.ocaml-compiler == '5.4.0' uses: actions/upload-artifact@v4 with: name: documentation path: _html retention-days: 1 - name: Run benchmarks run: make bench - name: Run benchmarks as JSON if: matrix.os == 'ubuntu-latest' && matrix.ocaml-compiler == '5.4.0' run: make bench-json - name: Store benchmark result if: matrix.os == 'ubuntu-latest' && matrix.ocaml-compiler == '5.4.0' uses: benchmark-action/github-action-benchmark@v1 with: name: server-reason-react Benchmarks tool: 'customBiggerIsBetter' output-file-path: bench_results.json github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: ${{ github.ref == 'refs/heads/main' }} gh-pages-branch: gh-pages benchmark-data-dir-path: dev/bench comment-always: true fail-on-alert: true comment-on-alert: true alert-comment-cc-users: '@davesnx' - name: Install dune-release if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-latest' && matrix.ocaml-compiler == '5.4.0' run: opam install dune-release -y - name: Release uses: davesnx/dune-release-action@v0.2.14 if: startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-latest' && matrix.ocaml-compiler == '5.4.0' with: packages: 'server-reason-react' changelog: './CHANGES.md' github-token: ${{ secrets.GH_TOKEN }} publish-docs: name: Publish documentation needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: name: github-pages url: https://ml-in-barcelona.github.io/server-reason-react permissions: contents: write pages: write id-token: write steps: - uses: actions/checkout@v4 - name: Download docs artifact uses: actions/download-artifact@v4 with: name: documentation path: _html - name: Publish to GitHub Pages uses: crazy-max/ghaction-github-pages@v1 with: target_branch: gh-pages build_dir: _html env: GITHUB_TOKEN: ${{ github.token }} ================================================ FILE: .github/workflows/docker.yml ================================================ name: Docker on: push: branches: - main pull_request: branches: - main concurrency: group: docker-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: build: name: Build Docker image runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build Docker image uses: docker/build-push-action@v6 with: context: . push: false tags: server-reason-react:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max ================================================ FILE: .gitignore ================================================ ### OS ### .DS_Store ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Dependency directories node_modules/ # Build **/dist/ # Optional npm cache directory .npm # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env *.annot *.cmo *.cma *.cmi *.a *.o *.cmx *.cmxs *.cmxa # dune working directory _build/ # odoc (odoc-driver) html artifacts _html/ # ocamlbuild targets *.byte *.native # Merlin configuring file for Vim and Emacs .merlin # Dune generated files *.install # Local OPAM switch _opam/ # vscode .vscode *.dump _build _opam _esy .bsb.lock .merlin .direnv/ demo/.running/* ================================================ FILE: .ocamlformat ================================================ profile=default margin=120 version=0.28.1 ================================================ FILE: CHANGES.md ================================================ # Changes ## 0.5.0 * Support Promise caching in react.client.components by @davesnx * Reorder head content exactly like react-dom/server by @davesnx * Implement hydration-compatible `useId` using React's tree-position-based algorithm, matching React 19 output. Adds `?identifier_prefix` to `renderToString`, `renderToStaticMarkup`, `renderToStream` and `render_html`. Fixes https://github.com/ml-in-barcelona/server-reason-react/issues/93 * Fix `renderToString` rendering Suspense children twice (once as trial, once with markers) due to side-effectful match expression. Children are now rendered into a separate buffer * Change shape for React.Event.* since Js.t is now supported. All methods fail at runtime with `Runtime.fail_impossible_action_in_ssr` * [server-reason-react.ppx] Strip units at any position (supporting mlx difference with [@JSX] transformations) * Add runtime error with clear message when `React.cloneElement` is used with uppercase components by @davesnx * Allow `[@platform js]` and `[@browser_only]` on externals to conditionally exclude them from native builds. Fixes https://github.com/ml-in-barcelona/server-reason-react/issues/170 by @davesnx * Generate `makeProps` in the PPX by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/364 * Implement `Js.t` natively with `Js.Internal` and a type registry by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/363 * Add `React.useActionState` by @davesnx * Add `key` into client components by @davesnx * Fix several functions in Belt to match specification (`Belt.Array.setExn`, `Belt.Array.concat`, `Belt.MutableMap.remove`, `Belt.HashMap.keepMapInPlace`, and avoid double callback evaluation) by @yasunariw in https://github.com/ml-in-barcelona/server-reason-react/pull/362 * Implement `Belt.Array.getUndefined` and annotate `Belt.Array.push` as not implemented by @davesnx * Remove deprecated folder from Belt and reorganise Belt tests by @davesnx * Fix leaking `was_previous` when node was closing by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/361 * Fix `React.cloneElement` on `Static {}` components by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/359 * Require ppxlib >= 0.36 by @davesnx ## 0.4.1 * Use OCaml 5.4.0 by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/335 * Use latest ppxlib by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/334 * Update to latest quickjs by @davesnx * Update dependency and usage by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/333 * Add filter to esbuild plugin to scope entrypoint by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/330 * Add back and forward navigation to nested router by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/329 * Implement memo and memoCustomCompareProps by @davesnx * Move Date, BigInt and modularise Js by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/327 * Create complex navigation at RSC demo by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/307 ## 0.4.0 * Add upper bound to quickjs 0.2.0 * Bump lwt to 5.9.2 * Expand styles prop into className and style props with optional handling by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/324 * Lowercase components have ?key:string by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/323 * Wrap client value on React.Upper_case_component by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/322 * Fix remove last element on nested_modules by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/321 * Add searchParams function to native URL by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/320 * Add URL construct function and improve lib build by @EmileTrotignon in https://github.com/ml-in-barcelona/server-reason-react/pull/317 * Specify model values at React by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/309 * Allow async in client props by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/315 * Improve the Fiber and Model stream context by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/312 * Align Suspense with reason-react by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/311 * Make client component to execute in runtime by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/306 * Fix mismatch of the model and html on render_html by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/305 * Fix createFromFetch interface and avoid transition on navigation by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/299 * Change ppx execution order (styles expansion in server-reason-react) by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/297 * Rename use function to usePromise in Experimental module by @pedrobslisboa in https://github.com/ml-in-barcelona/server-reason-react/pull/298 * Add shared-folder-prefix arg to ppx by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/294 ## 0.3.1 * Update quickjs dependency to 0.1.2 by @davesnx ## 0.3.0 * browser-ppx: process stritems by @jchavarri in https://github.com/ml-in-barcelona/server-reason-react/pull/127 * Make React.Children.* APIs work as expected by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/130 * Improve global crashes by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/132 * Support assets in `mel.module` by @jchavarri in https://github.com/ml-in-barcelona/server-reason-react/pull/134 * browser_only: don't convert to runtime errors on identifiers or function application by @jchavarri in https://github.com/ml-in-barcelona/server-reason-react/pull/138 * Port `j` quoted strings interpolation from Melange by @jchavarri in https://github.com/ml-in-barcelona/server-reason-react/pull/139 * mel.module: handle asset prefix by @jchavarri in https://github.com/ml-in-barcelona/server-reason-react/pull/140 * Add browser_only transformation to useEffect automatically by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/145 * Append doctype tag on html lowercase by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/136 * Transform Pexp_function with browser_only by @davesnx in https://github.com/ml-in-barcelona/server-reason-react/pull/146 ## 0.2.0 - Remove data-reactroot attr from ReactDOM.renderToString #129 by @pedrobslisboa - Make useUrl return the provided serverUrl #125 by @purefunctor - Replace Js.Re implemenation from `pcre` to quickjs b1a3e225cdad1298d705fbbd9618e15b0427ef0f by @davesnx - Remove Belt.Array.push #122 by @davesnx ## 0.1.0 Initial release of server-reason-react, includes: - Server-side rendering of ReasonReact components (renderToString, renderToStaticMarkup & renderToLwtStream) - `server-reason-react.browser_ppx` for skipping code from the server - `server-reason-react.melange_ppx` for enabling melange bindings and extensions which run on the server - `server-reason-react.belt` a native Belt implementation - `server-reason-react.js` a native Js implementation (unsafe and limited) - `server-reason-react.url` and `server-reason-react.url-native` a universal library with both implementations to work with URLs on the server and the client - `server-reason-react.promise` and `server-reason-react.promise-native` a universal library with both implementations to work with Promises on the server and the client. Based on https://github.com/aantron/promise - `server-reason-react.melange-fetch` a fork of melange-fetch which is a melange library to fetch data on the client via the Fetch API. This fork is to be able to compile it on the server (not running). - `server-reason-react.webapi` a fork of melange-webapi which is a melange library to work with the Web API on the client. This fork is to be able to compile it on the server (not running). ================================================ FILE: Dockerfile ================================================ FROM ocaml/opam:ubuntu-22.04-ocaml-5.4 AS builder RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends curl git libev-dev libssl-dev && \ sudo apt-get remove -y nodejs npm && sudo apt-get autoremove -y RUN curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && \ sudo apt-get update && \ sudo apt-get install -y --no-install-recommends nodejs && \ sudo npm install -g npm@latest RUN sudo ln -sf /usr/bin/opam-2.5 /usr/bin/opam && opam init --reinit -n WORKDIR /app RUN opam remote set-url default https://opam.ocaml.org RUN cd ~/opam-repository && git fetch -q origin master && git reset --hard origin/master && opam update -y COPY Makefile ./ COPY *.opam ./ COPY *.opam.template ./ COPY dune ./ COPY dune-project ./ RUN make pin RUN opam update -y && opam install . --deps-only -y && opam install dream -y WORKDIR /app/demo COPY demo/package.json ./package.json COPY demo/package-lock.json ./package-lock.json RUN sudo npm ci --omit=dev WORKDIR /app/demo/client COPY demo/client/package.json ./package.json COPY demo/client/package-lock.json ./package-lock.json RUN sudo npm install WORKDIR /app COPY . . RUN sudo chown -R opam:opam /app && opam exec -- dune build @demo --profile=dev RUN opam clean -a -c -s --logs && rm -rf /home/opam/opam-repository FROM ocaml/opam:ubuntu-22.04-ocaml-5.4 RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends libev4 libssl3 && \ sudo rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=builder --chown=opam:opam /home/opam/.opam /home/opam/.opam COPY --from=builder --chown=opam:opam /app /app ENV PATH="/home/opam/.opam/5.4/bin:${PATH}" EXPOSE 8080 CMD ["opam", "exec", "--switch", "5.4", "--", "_build/default/demo/server/server.exe"] ================================================ FILE: LICENSE.md ================================================ Copyright 2024 David Sancho & Javier Chavarri (ml-in-barcelona team) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ project_name = server-reason-react DUNE = opam exec -- dune opam_file = $(project_name).opam .PHONY: help help: ## Print this help message @echo ""; @echo "List of available make commands"; @echo ""; @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'; @echo ""; .PHONY: build build: ## Build the project, including non installable libraries and executables $(DUNE) build --profile=dev .PHONY: build-prod build-prod: ## Build for production (--profile=prod) $(DUNE) build --profile=prod .PHONY: dev dev: ## Build in watch mode $(DUNE) build -w --profile=dev .PHONY: clean clean: ## Clean artifacts $(DUNE) clean .PHONY: test test: ## Run the unit tests $(DUNE) build @runtest .PHONY: test-watch test-watch: ## Run the unit tests in watch mode $(DUNE) build @runtest -w .PHONY: test-promote test-promote: ## Updates snapshots and promotes it to correct $(DUNE) build @runtest --auto-promote .PHONY: format format: ## Format the codebase with ocamlformat @DUNE_CONFIG__GLOBAL_LOCK=disabled $(DUNE) build @fmt --auto-promote .PHONY: format-check format-check: ## Checks if format is correct @DUNE_CONFIG__GLOBAL_LOCK=disabled $(DUNE) build @fmt .PHONY: init setup-githooks: ## Setup githooks git config core.hooksPath .githooks .PHONY: create-switch create-switch: ## Create opam switch opam switch create . 5.4.0 --deps-only --with-test -y .PHONY: install install: opam install . --deps-only --with-test --with-doc --with-dev-setup -y .PHONY: install-npm install-npm: cd packages/esbuild-plugin && npm install; cd packages/react-server-dom-esbuild && npm install; cd demo && npm install; cd demo/client && npm install .PHONY: pin pin: ## Pin dependencies echo "No pins needed" .PHONY: init init: setup-githooks create-switch pin install install-npm ## Create a local dev enviroment .PHONY: ppx-test ppx-test: ## Run ppx tests $(DUNE) runtest packages/server-reason-react-ppx .PHONY: ppx-test-watch ppx-test-watch: ## Run ppx tests in watch mode $(DUNE) runtest packages/server-reason-react-ppx --watch .PHONY: ppx-test-promote ppx-test-promote: ## Prommote ppx tests snapshots $(DUNE) runtest packages/server-reason-react-ppx --auto-promote .PHONY: lib-test lib-test: ## Run library tests $(DUNE) exec test/test.exe .PHONY: demo-build demo-build: ## Build the project (client, server and universal) $(DUNE) build --profile=dev @demo .PHONY: demo-build-watch demo-build-watch: ## Watch demo (client, server and universal) $(DUNE) build --profile=dev @demo --force --watch .PHONY: demo demo-serve: demo-build ## Serve the demo executable @opam exec -- _build/default/demo/server/server.exe .PHONY: demo-serve-watch demo-serve-watch: ## Run demo executable on watch mode (listening to built_at.txt changes) @watchexec --no-vcs-ignore -w demo/.running/built_at.txt -r -c -- "_build/default/demo/server/server.exe" .PHONY: subst subst: ## Run dune substitute $(DUNE) subst .PHONY: docs docs: ## Generate odoc documentation $(DUNE) build @install $(DUNE) exec -- odoc_driver server-reason-react --remap # Because if the hack above, we can't have watch mode .PHONY: docs-watch docs-watch: ## Generate odoc docs $(DUNE) build --root . -w @doc-new --profile=prod .PHONY: docs-open docs-open: ## Open odoc docs with default web browser # open _build/default/_doc_new/html/docs/local/server-reason-react/index.html open _html/server-reason-react/index.html .PHONY: docs-serve docs-serve: docs docs-open ## Open odoc docs with default web browser .PHONY: build-bench build-bench: ## Build benchmark executables $(DUNE) build --profile=release benchmark/bench.exe .PHONY: bench bench: build-bench ## Run benchmarks @$(DUNE) exec benchmark/bench.exe --profile=release --display-separate-messages --no-print-directory .PHONY: bench-json bench-json: build-bench ## Run benchmarks with JSON output for CI @$(DUNE) exec benchmark/bench.exe --profile=release --display-separate-messages --no-print-directory -- --json > bench_results.json .PHONY: bench-watch bench-watch: build-bench ## Run benchmark in watch mode @$(DUNE) exec benchmark/bench.exe --profile=release --display-separate-messages --no-print-directory --watch .PHONY: bench-allocation bench-allocation: ## Run allocation analysis @$(DUNE) exec benchmark/allocation.exe --profile=release container_name = server-reason-react-demo current_hash = $(shell git rev-parse HEAD | cut -c1-7) .PHONY: docker-build docker-build: ## docker build DOCKER_BUILDKIT=0 docker build . --tag "$(container_name):$(current_hash)" --platform linux/amd64 .PHONY: docker-run docker-run: ## docker run @docker run -d --platform linux/amd64 $(container_name):$(current_hash) ================================================ FILE: README.md ================================================ # server-reason-react Native implementation of React's server-side rendering (SSR) and React Server Components (RSC) architecture for Reason. Designed to be used with [reason-react](https://github.com/reasonml/reason-react) and [Melange](https://github.com/melange-re/melange). Together it enables developers to write efficient React components using a single language, while target both native executable and JavaScript. ## Features - **Server-side rendering HTML** with `ReactDOM.renderToString`/`ReactDOM.renderToStaticMarkup` - Server-side rendering **streaming HTML** with `ReactDOM.renderToStream` (similar to react@18 `renderToReadableStream`) - Includes **`React.Suspense`** and **`React.use()`** implementations - **server-reason-react-ppx** - A ppx transformation to support JSX on native - All [reason-react](https://reasonml.github.io/reason-react/) interface is either implemented or stubbed (some of the methods, like React.useState need to be stubbed because they aren't used on the server!) - **React Server Components** - A ReactServerDOM module for streaming RSC payload, an esbuild plugin to enhance the bundle with client-components mappings, a Dream middleware to serve the RSC endpoint and a dummy implementation of a router (still [work in progress](https://github.com/ml-in-barcelona/server-reason-react/issues/204)) > Warning: This repo contains a few parts that are considered experimental and there's no guarantee of stability. Most of the stable parts are used in production at ahrefs.com, app.ahrefs.com and wordcount.com. Check each module's documentation for more details. ## Why There are plenty of motives for it, the main one is that [ahrefs](https://ahrefs.com) (the company I work for) needs it. We use OCaml for the backend and Reason (with React) for the frontend. We wanted to take advantage of the same features from React.js in the server as well. Currently 100% of the public site ([ahrefs.com](https://ahrefs.com)), the shell part of the dashboard ([app.ahrefs.com](https://app.ahrefs.com)) and [wordcount.com](https://wordcount.com) are rendered on the server with `server-reason-react`. What made us create this library was mostly: - Use the same language (Reason) for both server and client - Embrace server-client integrations such as type-safe routing, JSON decoding/encoding, sharing types and logic, while keep enjoying functional programming patterns - Native performance is better than JavaScript performance (Node.js, Bun, Deno) - Writing React from a different language than JavaScript, but still using the brilliant pieces from the ecosystem - Exploration of OCaml effects and React - Further exploration with OCaml multicore, direct-style and concurrency with React features such as async components, React.use or Suspense Explained more about the motivation in [this blog post](https://sancho.dev/blog/server-side-rendering-react-in-ocaml) and also in my talk about [**Universal react in OCaml** at fun-ocaml 2024](https://www.youtube.com/watch?v=Oy3lZl2kE-0&t=92s&ab_channel=FUNOCaml) and [**Server side rendering React natively with Reason** at ReactAlicante 2023](https://www.youtube.com/watch?v=e3qY-Eg9zRY&ab_channel=ReactAlicante) ## Other libraries inside this repo Aside from the core (`React`, `ReactDOM` and `ReactServerDOM`), server-reason-react repo contains some common melange libraries to ensure components are universal. Some of them are reimplementations in native of those libraries, and others are new implementations. Currently they are part of the repository, but eventually will be moved out to their own opam packages and repositories. | Name | Description | Melange equivalent library | |---------|-------------|---------| | [`server-reason-react.browser_ppx`](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/browser_only.html) | A ppx to discard code for each platform with different attributes: `let%browser_only`, `switch%platform` and `@platform` | | [`server-reason-react.url_js` and `server-reason-react.url_native`](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/server-reason-react.url_native/URL/index.html) | Universal URL module: binds to `window.URL` in browser, implemented with [`opam-uri`](https://github.com/mirage/ocaml-uri) in native | | [`server-reason-react.melange_ppx`](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/externals-melange-attributes.html) | A ppx to add the melange attributes to native code | [melange.ppx](https://melange.re/v4.0.0/) | | `server-reason-react.promise` | Vendored version of [aantron/promise](https://github.com/aantron/promise) with melange support [PR#80](https://github.com/aantron/promise/pull/80) | [promise](https://github.com/aantron/promise) | | `server-reason-react.belt` | Implementation of Belt for native [API reference](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/server-reason-react.belt_native/Belt/index.html) | [melange.belt](https://melange.re/v4.0.0/api/ml/melange/Belt) | | `server-reason-react.js` | Implementation of `Js` library for native (unsafe/incomplete). Check the issue [#110](https://github.com/ml-in-barcelona/server-reason-react/issues/110) for more details | [melange.js](https://melange.re/v4.0.0/api/ml/melange/Js) | | `server-reason-react.fetch` | Stub of fetch with browser_ppx to compile in native | [melange.fetch](https://github.com/melange-community/melange-fetch) | | `server-reason-react.webapi` | Stub version of Webapi library for native code compilation | [melange-webapi](https://github.com/melange-community/melange-webapi) | | `server-reason-react.dom` | Stub version of Dom library for native code compilation | [melange-dom](https://melange.re/v4.0.0/) | ## [Documentation](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/index.html) The [documentation site](https://ml-in-barcelona.github.io/server-reason-react/server-reason-react/index.html) is generated with odoc and hosted on GitHub Pages. ## Demo The `demo` folder contains a bunch of demos under a server to showcases the usages of `server-reason-react`. Check the [README](demo/README.md) for how to setup and run it. ## Want to contribute? [DM me](https://x.com/davesnx) ================================================ FILE: arch/browser/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: arch/browser/package.json ================================================ { "name": "cra", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: arch/browser/public/index.html ================================================ React App
================================================ FILE: arch/browser/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: arch/browser/src/index.css ================================================ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } ================================================ FILE: arch/browser/src/index.js ================================================ import React from 'react'; import ReactDOM from 'react-dom/client'; const App = () => { return (
); }; const root = ReactDOM.createRoot(window.document); root.render( ); ================================================ FILE: arch/server/head-ordering.js ================================================ /** * Reference file for React 19.1's head element ordering behavior. * Run: node head-ordering.js (from arch/server/) * * React's server renderer reorders children by priority bucket: * 1. meta[charset] * 2. meta[name="viewport"] * 3. link[rel="stylesheet"][precedence] (stylesheet resources) * 4. script[async][src] (async external scripts) * 5. everything else (title, regular meta, regular link, plain style, etc.) * * Within each bucket, elements maintain their relative discovery order. * Hoisted body elements appear before head-native elements within the same bucket. * * The RSC model keeps authored order; only the HTML shell is reordered. * React 19's hydration tolerates this mismatch via HostSingleton handling for . */ const React = require("react"); const { renderToPipeableStream } = require("react-dom/server"); const { Writable } = require("node:stream"); const el = React.createElement; function render(element, label) { return new Promise((resolve) => { let out = ""; const writable = new Writable({ write(chunk, _, cb) { out += chunk.toString(); cb(); }, }); const { pipe } = renderToPipeableStream(element, { onAllReady() { pipe(writable); }, onError(err) { console.error(label, err); process.exitCode = 1; }, }); writable.on("finish", () => { console.log(`\n=== ${label} ===`); console.log(out); resolve(); }); }); } async function main() { // Case 1: Issue #303 exact sample // Input order: style, link[precedence], meta[charset], meta[viewport] // Output order: meta[charset], meta[viewport], link[precedence], style await render( el( "html", { lang: "en" }, el( "head", null, el("style", null), el("link", { precedence: "low", rel: "stylesheet", href: "/foo.css" }), el("meta", { charSet: "utf-8" }), el("meta", { name: "viewport" }), ), el("body", null), ), "Case 1: Issue #303 sample", ); // Case 2: Mixed explicit + hoisted elements from await render( el( "html", null, el( "head", null, el("title", null, "My Page"), el("style", null, "body{margin:0}"), el("link", { rel: "stylesheet", href: "/a.css", precedence: "default", }), el("meta", { charSet: "utf-8" }), ), el( "body", null, el("link", { rel: "stylesheet", href: "/b.css", precedence: "high", }), el("meta", { name: "viewport", content: "width=device-width" }), el("title", null, "Override Title"), ), ), "Case 2: Mixed head + body hoistables", ); // Case 3: Broader bucket coverage await render( el( "html", null, el( "head", null, el("title", null, "App"), el("script", { async: true, src: "/app.js" }), el("link", { rel: "stylesheet", href: "/main.css", precedence: "default", }), el("meta", { name: "viewport", content: "width=device-width" }), el("meta", { charSet: "utf-8" }), el("link", { rel: "preconnect", href: "https://cdn.example.com" }), el("link", { rel: "stylesheet", href: "/theme.css", precedence: "low", }), el("style", null, ".app{color:red}"), el("meta", { name: "description", content: "A test app" }), ), el("body", null), ), "Case 3: Broad bucket coverage", ); } main(); ================================================ FILE: arch/server/package.json ================================================ { "name": "app", "version": "0.0.1", "scripts": { "react-dom-server": "bun react-dom-server.js", "render-html-to-stream": "bun render-html-to-stream.js", "render-rsc-to-stream": "bun --conditions react-server render-rsc-to-stream.js" }, "license": "MIT", "dependencies": { "react": "^19.1.0", "react-dom": "^19.1.0", "react-server-dom-webpack": "^19.1.0" } } ================================================ FILE: arch/server/react-dom-server-node-dom-props.js ================================================ var properties = {}; // These props are reserved by React. They shouldn't be written to the DOM. var reservedProps = [ "children", "dangerouslySetInnerHTML", // TODO: This prevents the assignment of defaultValue to regular // elements (not just inputs). Now that ReactDOMInput assigns to the // defaultValue property -- do we need this? "defaultValue", "defaultChecked", "innerHTML", "suppressContentEditableWarning", "suppressHydrationWarning", "style", ]; reservedProps.forEach(function (name) { properties[name] = new PropertyInfoRecord( name, RESERVED, false, // mustUseProperty name, // attributeName null, // attributeNamespace false ); }); // A few React string attributes have a different name. // This is a mapping from React prop names to the attribute names. [ ["acceptCharset", "accept-charset"], ["className", "class"], ["htmlFor", "for"], ["httpEquiv", "http-equiv"], ].forEach(function (_ref) { var name = _ref[0], attributeName = _ref[1]; properties[name] = new PropertyInfoRecord( name, STRING, false, // mustUseProperty attributeName, // attributeName null, // attributeNamespace false ); }); // These are "enumerated" HTML attributes that accept "true" and "false". // In React, we let users pass `true` and `false` even though technically // these aren't boolean attributes (they are coerced to strings). ["contentEditable", "draggable", "spellCheck", "value"].forEach(function ( name ) { properties[name] = new PropertyInfoRecord( name, BOOLEANISH_STRING, false, // mustUseProperty name.toLowerCase(), // attributeName null, // attributeNamespace false ); }); // These are "enumerated" SVG attributes that accept "true" and "false". // In React, we let users pass `true` and `false` even though technically // these aren't boolean attributes (they are coerced to strings). // Since these are SVG attributes, their attribute names are case-sensitive. [ "autoReverse", "externalResourcesRequired", "focusable", "preserveAlpha", ].forEach(function (name) { properties[name] = new PropertyInfoRecord( name, BOOLEANISH_STRING, false, // mustUseProperty name, // attributeName null, // attributeNamespace false ); }); // These are HTML boolean attributes. [ "allowFullScreen", "async", // Note: there is a special case that prevents it from being written to the DOM // on the client side because the browsers are inconsistent. Instead we call focus(). "autoFocus", "autoPlay", "controls", "default", "defer", "disabled", "disablePictureInPicture", "formNoValidate", "hidden", "loop", "noModule", "noValidate", "open", "playsInline", "readOnly", "required", "reversed", "scoped", "seamless", // Microdata "itemScope", ].forEach(function (name) { properties[name] = new PropertyInfoRecord( name, BOOLEAN, false, // mustUseProperty name.toLowerCase(), // attributeName null, // attributeNamespace false ); }); // These are the few React props that we set as DOM properties // rather than attributes. These are all booleans. [ "checked", // Note: `option.selected` is not updated if `select.multiple` is // disabled with `removeAttribute`. We have special logic for handling this. "multiple", "muted", "selected", // NOTE: if you add a camelCased prop to this list, // you'll need to set attributeName to name.toLowerCase() // instead in the assignment below. ].forEach(function (name) { properties[name] = new PropertyInfoRecord( name, BOOLEAN, true, // mustUseProperty name, // attributeName null, // attributeNamespace false ); }); // These are HTML attributes that are "overloaded booleans": they behave like // booleans, but can also accept a string value. [ "capture", "download", // NOTE: if you add a camelCased prop to this list, // you'll need to set attributeName to name.toLowerCase() // instead in the assignment below. ].forEach(function (name) { properties[name] = new PropertyInfoRecord( name, OVERLOADED_BOOLEAN, false, // mustUseProperty name, // attributeName null, // attributeNamespace false ); }); // These are HTML attributes that must be positive numbers. [ "cols", "rows", "size", "span", // NOTE: if you add a camelCased prop to this list, // you'll need to set attributeName to name.toLowerCase() // instead in the assignment below. ].forEach(function (name) { properties[name] = new PropertyInfoRecord( name, POSITIVE_NUMERIC, false, // mustUseProperty name, // attributeName null, // attributeNamespace false ); }); // These are HTML attributes that must be numbers. ["rowSpan", "start"].forEach(function (name) { properties[name] = new PropertyInfoRecord( name, NUMERIC, false, // mustUseProperty name.toLowerCase(), // attributeName null, // attributeNamespace false ); }); var CAMELIZE = /[\-\:]([a-z])/g; var capitalize = function (token) { return token[1].toUpperCase(); }; // This is a list of all SVG attributes that need special casing, namespacing, // or boolean value assignment. Regular attributes that just accept strings // and have the same names are omitted, just like in the HTML whitelist. // Some of these attributes can be hard to find. This list was created by // scraping the MDN documentation. [ "accent-height", "alignment-baseline", "arabic-form", "baseline-shift", "cap-height", "clip-path", "clip-rule", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "dominant-baseline", "enable-background", "fill-opacity", "fill-rule", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "glyph-name", "glyph-orientation-horizontal", "glyph-orientation-vertical", "horiz-adv-x", "horiz-origin-x", "image-rendering", "letter-spacing", "lighting-color", "marker-end", "marker-mid", "marker-start", "overline-position", "overline-thickness", "paint-order", "panose-1", "pointer-events", "rendering-intent", "shape-rendering", "stop-color", "stop-opacity", "strikethrough-position", "strikethrough-thickness", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-anchor", "text-decoration", "text-rendering", "underline-position", "underline-thickness", "unicode-bidi", "unicode-range", "units-per-em", "v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical", "vector-effect", "vert-adv-y", "vert-origin-x", "vert-origin-y", "word-spacing", "writing-mode", "xmlns:xlink", "x-height", // NOTE: if you add a camelCased prop to this list, // you'll need to set attributeName to name.toLowerCase() // instead in the assignment below. ].forEach(function (attributeName) { var name = attributeName.replace(CAMELIZE, capitalize); properties[name] = new PropertyInfoRecord( name, STRING, false, // mustUseProperty attributeName, null, // attributeNamespace false ); }); // String SVG attributes with the xlink namespace. [ "xlink:actuate", "xlink:arcrole", "xlink:role", "xlink:show", "xlink:title", "xlink:type", // NOTE: if you add a camelCased prop to this list, // you'll need to set attributeName to name.toLowerCase() // instead in the assignment below. ].forEach(function (attributeName) { var name = attributeName.replace(CAMELIZE, capitalize); properties[name] = new PropertyInfoRecord( name, STRING, false, // mustUseProperty attributeName, "http://www.w3.org/1999/xlink", false ); }); // String SVG attributes with the xml namespace. [ "xml:base", "xml:lang", "xml:space", // NOTE: if you add a camelCased prop to this list, // you'll need to set attributeName to name.toLowerCase() // instead in the assignment below. ].forEach(function (attributeName) { var name = attributeName.replace(CAMELIZE, capitalize); properties[name] = new PropertyInfoRecord( name, STRING, false, // mustUseProperty attributeName, "http://www.w3.org/XML/1998/namespace", false ); }); // These attribute exists both in HTML and SVG. // The attribute name is case-sensitive in SVG so we can't just use // the React name like we do for attributes that exist only in HTML. ["tabIndex", "crossOrigin"].forEach(function (attributeName) { properties[attributeName] = new PropertyInfoRecord( attributeName, STRING, false, // mustUseProperty attributeName.toLowerCase(), // attributeName null, // attributeNamespace false ); }); // These attributes accept URLs. These must not allow javascript: URLS. // These will also need to accept Trusted Types object in the future. var xlinkHref = "xlinkHref"; properties[xlinkHref] = new PropertyInfoRecord( "xlinkHref", STRING, false, // mustUseProperty "xlink:href", "http://www.w3.org/1999/xlink", true ); ["src", "href", "action", "formAction"].forEach(function (attributeName) { properties[attributeName] = new PropertyInfoRecord( attributeName, STRING, false, // mustUseProperty attributeName.toLowerCase(), // attributeName null, // attributeNamespace true ); }); /* */ // When adding attributes to the HTML or SVG whitelist, be sure to // also add them to this module to ensure casing and incorrect name // warnings. var possibleStandardNames = { // HTML accept: "accept", acceptcharset: "acceptCharset", "accept-charset": "acceptCharset", accesskey: "accessKey", action: "action", allowfullscreen: "allowFullScreen", alt: "alt", as: "as", async: "async", autocapitalize: "autoCapitalize", autocomplete: "autoComplete", autocorrect: "autoCorrect", autofocus: "autoFocus", autoplay: "autoPlay", autosave: "autoSave", capture: "capture", cellpadding: "cellPadding", cellspacing: "cellSpacing", challenge: "challenge", charset: "charSet", checked: "checked", children: "children", cite: "cite", class: "className", classid: "classID", classname: "className", cols: "cols", colspan: "colSpan", content: "content", contenteditable: "contentEditable", contextmenu: "contextMenu", controls: "controls", controlslist: "controlsList", coords: "coords", crossorigin: "crossOrigin", dangerouslysetinnerhtml: "dangerouslySetInnerHTML", data: "data", datetime: "dateTime", default: "default", defaultchecked: "defaultChecked", defaultvalue: "defaultValue", defer: "defer", dir: "dir", disabled: "disabled", disablepictureinpicture: "disablePictureInPicture", download: "download", draggable: "draggable", enctype: "encType", for: "htmlFor", form: "form", formmethod: "formMethod", formaction: "formAction", formenctype: "formEncType", formnovalidate: "formNoValidate", formtarget: "formTarget", frameborder: "frameBorder", headers: "headers", height: "height", hidden: "hidden", high: "high", href: "href", hreflang: "hrefLang", htmlfor: "htmlFor", httpequiv: "httpEquiv", "http-equiv": "httpEquiv", icon: "icon", id: "id", innerhtml: "innerHTML", inputmode: "inputMode", integrity: "integrity", is: "is", itemid: "itemID", itemprop: "itemProp", itemref: "itemRef", itemscope: "itemScope", itemtype: "itemType", keyparams: "keyParams", keytype: "keyType", kind: "kind", label: "label", lang: "lang", list: "list", loop: "loop", low: "low", manifest: "manifest", marginwidth: "marginWidth", marginheight: "marginHeight", max: "max", maxlength: "maxLength", media: "media", mediagroup: "mediaGroup", method: "method", min: "min", minlength: "minLength", multiple: "multiple", muted: "muted", name: "name", nomodule: "noModule", nonce: "nonce", novalidate: "noValidate", open: "open", optimum: "optimum", pattern: "pattern", placeholder: "placeholder", playsinline: "playsInline", poster: "poster", preload: "preload", profile: "profile", radiogroup: "radioGroup", readonly: "readOnly", referrerpolicy: "referrerPolicy", rel: "rel", required: "required", reversed: "reversed", role: "role", rows: "rows", rowspan: "rowSpan", sandbox: "sandbox", scope: "scope", scoped: "scoped", scrolling: "scrolling", seamless: "seamless", selected: "selected", shape: "shape", size: "size", sizes: "sizes", span: "span", spellcheck: "spellCheck", src: "src", srcdoc: "srcDoc", srclang: "srcLang", srcset: "srcSet", start: "start", step: "step", style: "style", summary: "summary", tabindex: "tabIndex", target: "target", title: "title", type: "type", usemap: "useMap", value: "value", width: "width", wmode: "wmode", wrap: "wrap", // SVG about: "about", accentheight: "accentHeight", "accent-height": "accentHeight", accumulate: "accumulate", additive: "additive", alignmentbaseline: "alignmentBaseline", "alignment-baseline": "alignmentBaseline", allowreorder: "allowReorder", alphabetic: "alphabetic", amplitude: "amplitude", arabicform: "arabicForm", "arabic-form": "arabicForm", ascent: "ascent", attributename: "attributeName", attributetype: "attributeType", autoreverse: "autoReverse", azimuth: "azimuth", basefrequency: "baseFrequency", baselineshift: "baselineShift", "baseline-shift": "baselineShift", baseprofile: "baseProfile", bbox: "bbox", begin: "begin", bias: "bias", by: "by", calcmode: "calcMode", capheight: "capHeight", "cap-height": "capHeight", clip: "clip", clippath: "clipPath", "clip-path": "clipPath", clippathunits: "clipPathUnits", cliprule: "clipRule", "clip-rule": "clipRule", color: "color", colorinterpolation: "colorInterpolation", "color-interpolation": "colorInterpolation", colorinterpolationfilters: "colorInterpolationFilters", "color-interpolation-filters": "colorInterpolationFilters", colorprofile: "colorProfile", "color-profile": "colorProfile", colorrendering: "colorRendering", "color-rendering": "colorRendering", contentscripttype: "contentScriptType", contentstyletype: "contentStyleType", cursor: "cursor", cx: "cx", cy: "cy", d: "d", datatype: "datatype", decelerate: "decelerate", descent: "descent", diffuseconstant: "diffuseConstant", direction: "direction", display: "display", divisor: "divisor", dominantbaseline: "dominantBaseline", "dominant-baseline": "dominantBaseline", dur: "dur", dx: "dx", dy: "dy", edgemode: "edgeMode", elevation: "elevation", enablebackground: "enableBackground", "enable-background": "enableBackground", end: "end", exponent: "exponent", externalresourcesrequired: "externalResourcesRequired", fill: "fill", fillopacity: "fillOpacity", "fill-opacity": "fillOpacity", fillrule: "fillRule", "fill-rule": "fillRule", filter: "filter", filterres: "filterRes", filterunits: "filterUnits", floodopacity: "floodOpacity", "flood-opacity": "floodOpacity", floodcolor: "floodColor", "flood-color": "floodColor", focusable: "focusable", fontfamily: "fontFamily", "font-family": "fontFamily", fontsize: "fontSize", "font-size": "fontSize", fontsizeadjust: "fontSizeAdjust", "font-size-adjust": "fontSizeAdjust", fontstretch: "fontStretch", "font-stretch": "fontStretch", fontstyle: "fontStyle", "font-style": "fontStyle", fontvariant: "fontVariant", "font-variant": "fontVariant", fontweight: "fontWeight", "font-weight": "fontWeight", format: "format", from: "from", fx: "fx", fy: "fy", g1: "g1", g2: "g2", glyphname: "glyphName", "glyph-name": "glyphName", glyphorientationhorizontal: "glyphOrientationHorizontal", "glyph-orientation-horizontal": "glyphOrientationHorizontal", glyphorientationvertical: "glyphOrientationVertical", "glyph-orientation-vertical": "glyphOrientationVertical", glyphref: "glyphRef", gradienttransform: "gradientTransform", gradientunits: "gradientUnits", hanging: "hanging", horizadvx: "horizAdvX", "horiz-adv-x": "horizAdvX", horizoriginx: "horizOriginX", "horiz-origin-x": "horizOriginX", ideographic: "ideographic", imagerendering: "imageRendering", "image-rendering": "imageRendering", in2: "in2", in: "in", inlist: "inlist", intercept: "intercept", k1: "k1", k2: "k2", k3: "k3", k4: "k4", k: "k", kernelmatrix: "kernelMatrix", kernelunitlength: "kernelUnitLength", kerning: "kerning", keypoints: "keyPoints", keysplines: "keySplines", keytimes: "keyTimes", lengthadjust: "lengthAdjust", letterspacing: "letterSpacing", "letter-spacing": "letterSpacing", lightingcolor: "lightingColor", "lighting-color": "lightingColor", limitingconeangle: "limitingConeAngle", local: "local", markerend: "markerEnd", "marker-end": "markerEnd", markerheight: "markerHeight", markermid: "markerMid", "marker-mid": "markerMid", markerstart: "markerStart", "marker-start": "markerStart", markerunits: "markerUnits", markerwidth: "markerWidth", mask: "mask", maskcontentunits: "maskContentUnits", maskunits: "maskUnits", mathematical: "mathematical", mode: "mode", numoctaves: "numOctaves", offset: "offset", opacity: "opacity", operator: "operator", order: "order", orient: "orient", orientation: "orientation", origin: "origin", overflow: "overflow", overlineposition: "overlinePosition", "overline-position": "overlinePosition", overlinethickness: "overlineThickness", "overline-thickness": "overlineThickness", paintorder: "paintOrder", "paint-order": "paintOrder", panose1: "panose1", "panose-1": "panose1", pathlength: "pathLength", patterncontentunits: "patternContentUnits", patterntransform: "patternTransform", patternunits: "patternUnits", pointerevents: "pointerEvents", "pointer-events": "pointerEvents", points: "points", pointsatx: "pointsAtX", pointsaty: "pointsAtY", pointsatz: "pointsAtZ", prefix: "prefix", preservealpha: "preserveAlpha", preserveaspectratio: "preserveAspectRatio", primitiveunits: "primitiveUnits", property: "property", r: "r", radius: "radius", refx: "refX", refy: "refY", renderingintent: "renderingIntent", "rendering-intent": "renderingIntent", repeatcount: "repeatCount", repeatdur: "repeatDur", requiredextensions: "requiredExtensions", requiredfeatures: "requiredFeatures", resource: "resource", restart: "restart", result: "result", results: "results", rotate: "rotate", rx: "rx", ry: "ry", scale: "scale", security: "security", seed: "seed", shaperendering: "shapeRendering", "shape-rendering": "shapeRendering", slope: "slope", spacing: "spacing", specularconstant: "specularConstant", specularexponent: "specularExponent", speed: "speed", spreadmethod: "spreadMethod", startoffset: "startOffset", stddeviation: "stdDeviation", stemh: "stemh", stemv: "stemv", stitchtiles: "stitchTiles", stopcolor: "stopColor", "stop-color": "stopColor", stopopacity: "stopOpacity", "stop-opacity": "stopOpacity", strikethroughposition: "strikethroughPosition", "strikethrough-position": "strikethroughPosition", strikethroughthickness: "strikethroughThickness", "strikethrough-thickness": "strikethroughThickness", string: "string", stroke: "stroke", strokedasharray: "strokeDasharray", "stroke-dasharray": "strokeDasharray", strokedashoffset: "strokeDashoffset", "stroke-dashoffset": "strokeDashoffset", strokelinecap: "strokeLinecap", "stroke-linecap": "strokeLinecap", strokelinejoin: "strokeLinejoin", "stroke-linejoin": "strokeLinejoin", strokemiterlimit: "strokeMiterlimit", "stroke-miterlimit": "strokeMiterlimit", strokewidth: "strokeWidth", "stroke-width": "strokeWidth", strokeopacity: "strokeOpacity", "stroke-opacity": "strokeOpacity", suppresscontenteditablewarning: "suppressContentEditableWarning", suppresshydrationwarning: "suppressHydrationWarning", surfacescale: "surfaceScale", systemlanguage: "systemLanguage", tablevalues: "tableValues", targetx: "targetX", targety: "targetY", textanchor: "textAnchor", "text-anchor": "textAnchor", textdecoration: "textDecoration", "text-decoration": "textDecoration", textlength: "textLength", textrendering: "textRendering", "text-rendering": "textRendering", to: "to", transform: "transform", typeof: "typeof", u1: "u1", u2: "u2", underlineposition: "underlinePosition", "underline-position": "underlinePosition", underlinethickness: "underlineThickness", "underline-thickness": "underlineThickness", unicode: "unicode", unicodebidi: "unicodeBidi", "unicode-bidi": "unicodeBidi", unicoderange: "unicodeRange", "unicode-range": "unicodeRange", unitsperem: "unitsPerEm", "units-per-em": "unitsPerEm", unselectable: "unselectable", valphabetic: "vAlphabetic", "v-alphabetic": "vAlphabetic", values: "values", vectoreffect: "vectorEffect", "vector-effect": "vectorEffect", version: "version", vertadvy: "vertAdvY", "vert-adv-y": "vertAdvY", vertoriginx: "vertOriginX", "vert-origin-x": "vertOriginX", vertoriginy: "vertOriginY", "vert-origin-y": "vertOriginY", vhanging: "vHanging", "v-hanging": "vHanging", videographic: "vIdeographic", "v-ideographic": "vIdeographic", viewbox: "viewBox", viewtarget: "viewTarget", visibility: "visibility", vmathematical: "vMathematical", "v-mathematical": "vMathematical", vocab: "vocab", widths: "widths", wordspacing: "wordSpacing", "word-spacing": "wordSpacing", writingmode: "writingMode", "writing-mode": "writingMode", x1: "x1", x2: "x2", x: "x", xchannelselector: "xChannelSelector", xheight: "xHeight", "x-height": "xHeight", xlinkactuate: "xlinkActuate", "xlink:actuate": "xlinkActuate", xlinkarcrole: "xlinkArcrole", "xlink:arcrole": "xlinkArcrole", xlinkhref: "xlinkHref", "xlink:href": "xlinkHref", xlinkrole: "xlinkRole", "xlink:role": "xlinkRole", xlinkshow: "xlinkShow", "xlink:show": "xlinkShow", xlinktitle: "xlinkTitle", "xlink:title": "xlinkTitle", xlinktype: "xlinkType", "xlink:type": "xlinkType", xmlbase: "xmlBase", "xml:base": "xmlBase", xmllang: "xmlLang", "xml:lang": "xmlLang", xmlns: "xmlns", "xml:space": "xmlSpace", xmlnsxlink: "xmlnsXlink", "xmlns:xlink": "xmlnsXlink", xmlspace: "xmlSpace", y1: "y1", y2: "y2", y: "y", ychannelselector: "yChannelSelector", z: "z", zoomandpan: "zoomAndPan", }; /* */ ================================================ FILE: arch/server/react-dom-server.js ================================================ const React = require("react"); const ReactDOM = require("react-dom/server"); let first = React.createElement( "div", { key: "fi", value1: "aaa", style: { margin: "1px" } }, ["first"], ); let clon = React.cloneElement( first, { value: "bbb", more: 22, style: { margin: "1", padding: "0px" } }, ["asdf"], ); const { Provider, Consumer } = React.createContext(10); var app = () => { /* https://fb.me/react-uselayouteffect-ssr */ React.useLayoutEffect(() => { console.log("asdfdsf"); return () => { console.log("asdfsdf"); }; }); return React.createElement("div", { className: "contenido" }, []); }; var app = () => { let [state, setState] = React.useState(0); React.useEffect(() => { setState(state + 1); console.log("asdfdsf"); return () => { console.log("asdfsdf"); }; }); return React.createElement("div", { className: "contenido" }, []); }; var app = () => { let [state, setState] = React.useState(0); let ref = React.useRef(true); console.log(state); if (ref.current) { setState(state + 1); ref.current = false; } React.useEffect(() => { console.log("asfsdafsafsadf"); }); React.useEffect(() => { console.log("asfsdafsafsadf"); }, [state]); console.log(state); return React.createElement("div", null, [state]); }; /* var app = () => { return React.createElement("div", { dangerouslySetInnerHTML: { __html: "asdf" }, }); }; */ var ctx = React.createContext(10); var context_user = () => { let a = React.useContext(ctx); console.log(a); return React.createElement("div", { key: 1 }, [a]); }; var app = () => { let ref = React.useRef(333); console.log(ref); return React.createElement( ctx.Provider, { value: 0, ref: ref }, React.createElement(context_user), ); }; var app = React.forwardRef(() => { let ref = React.useRef(333); console.log(ref); return React.createElement( ctx.Provider, { value: 0, ref: ref }, React.createElement(context_user), ); }); var app = () => { return React.createElement( "script", { "aria-hidden": "true", }, `console.log("asdfas");`, ); }; function App() { let value = "asdfasdf"; return ; } console.log(ReactDOM.renderToStaticMarkup()); ================================================ FILE: arch/server/render-html-to-stream.js ================================================ import React from "react"; import * as ReactDOM from "react-dom/server"; import { prefetchDNS, preconnect, preload, preinit } from 'react-dom' const sleep = (seconds) => new Promise((res) => setTimeout(res, seconds * 1000)); const DeferredComponent = async ({ by, children }) => { await sleep(by); return (
Sleep {by}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 App = () => ( "lol" ); */ /* const App = () => (
"lol"
); */ /* const AlwaysThrow = () => { throw new Error("always throwing"); }; const App = () => ( ); */ /* function App() { return React.createElement( Suspense, { fallback: "Fallback 1" }, React.createElement(DeferredComponent, { by: 0.02 }, React.createElement( Suspense, { fallback: "Fallback 2" }, React.createElement(DeferredComponent, { by: 0.02 }, "lol" ) ) ) ); } */ /* function App() { return ( "asdf"
lol
); } */ /* const AnotherComponent = async () => { preinit('analytics.js', { as: 'script' }); await sleep(1); return <> } const App = () => { return (
) }; ReactDOM.renderToReadableStream().then((stream) => { debug(stream); }); ================================================ FILE: arch/server/render-rsc-to-stream.js ================================================ import React from "react"; import { renderToPipeableStream } from "react-server-dom-webpack/server"; import { prefetchDNS, preconnect, preload, preinit } from 'react-dom' 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 reject = (_) => new Promise((_res, rej) => { return rej(0) }); const AlwaysError = () => { throw new Error("lol"); }; let Await_tick = ({ num }) => { return num } const App = () => { return (
) }; const { pipe } = renderToPipeableStream(); pipe(process.stdout); /* https://codesandbox.io/p/sandbox/vibrant-voice-hdrlzt?file=%2Fsrc%2FApp.js */ ================================================ FILE: arch/server/test-useid-edge-cases.js ================================================ const React = require("react"); const ReactDOM = require("react-dom/server"); function DivWithId({ label }) { const id = React.useId(); return React.createElement("div", { id, "data-label": label }); } // ── Edge Case 1: useId inside Suspense children ────────────────────── // (sync component, no actual suspension) function SuspenseWithUseId() { return React.createElement( React.Suspense, { fallback: React.createElement("div", null, "loading") }, React.createElement(DivWithId, { label: "inside-suspense" }), ); } // ── Edge Case 2: useId in Suspense fallback vs children ────────────── // Does Suspense fork the tree context? function FallbackWithId() { const id = React.useId(); return React.createElement("div", { id, "data-label": "fallback" }); } function SuspenseWithIdInBoth() { return React.createElement( "div", null, React.createElement( React.Suspense, { fallback: React.createElement(FallbackWithId), }, React.createElement(DivWithId, { label: "content" }), ), React.createElement(DivWithId, { label: "sibling" }), ); } // ── Edge Case 3: Fragment wrapping ─────────────────────────────────── // Does Fragment affect tree context? function FragmentTest() { return React.createElement( "div", null, React.createElement( React.Fragment, null, React.createElement(DivWithId, { label: "in-fragment" }), ), ); } // ── Edge Case 4: Fragment with multiple children ───────────────────── function FragmentMultipleChildren() { return React.createElement( "div", null, React.createElement( React.Fragment, null, React.createElement(DivWithId, { label: "frag-child-1" }), React.createElement(DivWithId, { label: "frag-child-2" }), ), ); } // ── Edge Case 5: Nested fragments ──────────────────────────────────── function NestedFragments() { return React.createElement( "div", null, React.createElement( React.Fragment, null, React.createElement( React.Fragment, null, React.createElement(DivWithId, { label: "nested-frag" }), ), ), ); } // ── Edge Case 6: Empty elements between components ─────────────────── function NullBetween() { return React.createElement( "div", null, React.createElement(DivWithId, { label: "before-null" }), null, React.createElement(DivWithId, { label: "after-null" }), ); } // ── Edge Case 7: useId with conditional children ───────────────────── function ConditionalChildren({ show }) { return React.createElement( "div", null, show ? React.createElement(DivWithId, { label: "conditional" }) : null, React.createElement(DivWithId, { label: "always" }), ); } // ── Edge Case 8: useId with keyed fragments ────────────────────────── function KeyedFragments() { return React.createElement( "div", null, React.createElement( React.Fragment, { key: "a" }, React.createElement(DivWithId, { label: "keyed-a" }), ), React.createElement( React.Fragment, { key: "b" }, React.createElement(DivWithId, { label: "keyed-b" }), ), ); } // ── Edge Case 9: Deeply nested list (many siblings) ────────────────── function ManySiblings() { const items = Array.from({ length: 10 }, (_, i) => React.createElement(DivWithId, { key: String(i), label: `item-${i}` }), ); return React.createElement("div", null, items); } // ── Edge Case 10: useId after Suspense boundary ───────────────────── function UseIdAfterSuspense() { return React.createElement( "div", null, React.createElement( React.Suspense, { fallback: React.createElement("div", null, "loading") }, React.createElement(DivWithId, { label: "inside" }), ), React.createElement(DivWithId, { label: "after-suspense" }), ); } // ── Edge Case 11: Provider/Context with useId ──────────────────────── const MyContext = React.createContext("default"); function ProviderWithUseId() { return React.createElement( MyContext.Provider, { value: "provided" }, React.createElement(DivWithId, { label: "inside-provider" }), ); } // ── Edge Case 12: Mixed: Provider, Fragment, Suspense ──────────────── function KitchenSink() { return React.createElement( "div", null, React.createElement( MyContext.Provider, { value: "a" }, React.createElement( React.Fragment, null, React.createElement(DivWithId, { label: "provider-frag-1" }), React.createElement( React.Suspense, { fallback: React.createElement("span", null, "..."), }, React.createElement(DivWithId, { label: "provider-suspense" }), ), ), ), React.createElement(DivWithId, { label: "outside" }), ); } const tests = [ ["Edge 1: useId inside Suspense (sync)", SuspenseWithUseId], ["Edge 2: Suspense with useId in content + sibling", SuspenseWithIdInBoth], ["Edge 3: Fragment wrapping (single child)", FragmentTest], ["Edge 4: Fragment with multiple children", FragmentMultipleChildren], ["Edge 5: Nested fragments", NestedFragments], ["Edge 6: Null between components", NullBetween], ["Edge 8: Keyed fragments", KeyedFragments], ["Edge 9: Many siblings (10)", ManySiblings], ["Edge 10: useId after Suspense", UseIdAfterSuspense], ["Edge 11: Provider with useId", ProviderWithUseId], ["Edge 12: Kitchen sink", KitchenSink], ]; for (const [name, Component] of tests) { const html = ReactDOM.renderToString(React.createElement(Component)); console.log(`${name}:`); console.log(` ${html}`); console.log(); } // Edge 7 needs two renders console.log("Edge 7a: Conditional children (show=true):"); console.log( ` ${ReactDOM.renderToString(React.createElement(ConditionalChildren, { show: true }))}`, ); console.log(); console.log("Edge 7b: Conditional children (show=false):"); console.log( ` ${ReactDOM.renderToString(React.createElement(ConditionalChildren, { show: false }))}`, ); ================================================ FILE: arch/server/test-useid.js ================================================ const React = require("react"); const ReactDOM = require("react-dom/server"); // Helper: component that renders a div with its useId value function DivWithId() { const id = React.useId(); return React.createElement("div", { id: id }); } // Helper: component that calls useId twice function DivWithTwoIds() { const id1 = React.useId(); const id2 = React.useId(); return React.createElement("div", { "data-id1": id1, "data-id2": id2 }); } // Helper: component that calls useId three times function DivWithThreeIds() { const id1 = React.useId(); const id2 = React.useId(); const id3 = React.useId(); return React.createElement("div", { "data-id1": id1, "data-id2": id2, "data-id3": id3, }); } // Helper: wrapper component (no useId) function Wrapper({ children }) { return React.createElement("div", { className: "wrapper" }, children); } // Helper: component with useId that wraps children function ParentWithId({ children }) { const id = React.useId(); return React.createElement("div", { id: id }, children); } // Test 1: Single component with useId function Test1() { return React.createElement(DivWithId); } // Test 2: Two sibling components with useId function Test2() { return React.createElement( "div", null, React.createElement(DivWithId), React.createElement(DivWithId), ); } // Test 3: Nested components with useId function Test3() { return React.createElement( ParentWithId, null, React.createElement(DivWithId), ); } // Test 4: Multiple useId calls in one component function Test4() { return React.createElement(DivWithTwoIds); } // Test 5: Three useId calls in one component function Test5() { return React.createElement(DivWithThreeIds); } // Test 6: Siblings with nested children function Test6() { return React.createElement( "div", null, React.createElement( ParentWithId, null, React.createElement(DivWithId), ), React.createElement(DivWithId), ); } // Test 7: Deep nesting (3 levels) function Test7() { return React.createElement( ParentWithId, null, React.createElement( ParentWithId, null, React.createElement(DivWithId), ), ); } // Test 8: Wrapper (no useId) doesn't affect IDs function Test8() { return React.createElement( Wrapper, null, React.createElement(DivWithId), ); } // Test 9: Three siblings function Test9() { return React.createElement( "div", null, React.createElement(DivWithId), React.createElement(DivWithId), React.createElement(DivWithId), ); } // Test 10: Sibling components each with nested children function Test10() { return React.createElement( "div", null, React.createElement( ParentWithId, null, React.createElement(DivWithId), React.createElement(DivWithId), ), React.createElement( ParentWithId, null, React.createElement(DivWithId), ), ); } // Test 11: Array/list of components (dynamic children via array) function Test11() { const items = [0, 1, 2].map((i) => React.createElement(DivWithId, { key: String(i) }), ); return React.createElement("div", null, items); } // Test 12: Separate renderToString calls produce same IDs (reset per render) function Test12a() { return React.createElement(DivWithId); } function Test12b() { return React.createElement(DivWithId); } const tests = [ ["Test 1: Single component with useId", Test1], ["Test 2: Two sibling components", Test2], ["Test 3: Nested components", Test3], ["Test 4: Two useId calls in one component", Test4], ["Test 5: Three useId calls in one component", Test5], ["Test 6: Siblings with nested children", Test6], ["Test 7: Deep nesting (3 levels)", Test7], ["Test 8: Wrapper without useId", Test8], ["Test 9: Three siblings", Test9], ["Test 10: Complex siblings with nested", Test10], ["Test 11: Array/list of components", Test11], ]; for (const [name, Component] of tests) { const html = ReactDOM.renderToString(React.createElement(Component)); console.log(`${name}:`); console.log(` ${html}`); console.log(); } // Test 12: Separate renders console.log("Test 12: Separate renders produce same IDs:"); const html12a = ReactDOM.renderToString(React.createElement(Test12a)); const html12b = ReactDOM.renderToString(React.createElement(Test12b)); console.log(` render1: ${html12a}`); console.log(` render2: ${html12b}`); console.log(` same: ${html12a === html12b}`); ================================================ FILE: benchmark/Makefile ================================================ # Server Reason React Benchmark Suite # ================================== .PHONY: all build build-native build-js clean help .PHONY: bench bench-micro bench-memory bench-streaming bench-http .PHONY: bench-render bench-render-native bench-render-bun bench-render-node .PHONY: native-server js-servers # Directories BENCHMARK_DIR := $(shell pwd) PROJECT_ROOT := $(shell dirname $(BENCHMARK_DIR)) RESULTS_DIR := $(BENCHMARK_DIR)/results FRAMEWORKS_DIR := $(BENCHMARK_DIR)/frameworks BUN := $(shell command -v bun 2>/dev/null || echo "$(HOME)/.bun/bin/bun") # Optimization control # Set DISABLE_OPTIMIZATIONS=1 to build without optimizations (--profile=dev) # Default: optimizations enabled (--profile=release) ifeq ($(DISABLE_OPTIMIZATIONS),1) DUNE_PROFILE := --profile=dev else DUNE_PROFILE := --profile=release endif # Default target all: help # ============================================================================ # Build Targets # ============================================================================ build: build-native build-js @echo "✓ All builds complete" build-native: @echo "Building native OCaml benchmarks..." @if [ "$(DISABLE_OPTIMIZATIONS)" = "1" ]; then \ echo " (Optimizations DISABLED)"; \ else \ echo " (Optimizations ENABLED)"; \ fi cd $(PROJECT_ROOT) && opam exec -- dune build $(DUNE_PROFILE) benchmark/scenarios/benchmark_scenarios.a cd $(PROJECT_ROOT) && opam exec -- dune build $(DUNE_PROFILE) benchmark/native/server.exe cd $(PROJECT_ROOT) && opam exec -- dune build $(DUNE_PROFILE) benchmark/memory/memory_bench.exe cd $(PROJECT_ROOT) && opam exec -- dune build $(DUNE_PROFILE) benchmark/streaming/streaming_bench.exe @echo "✓ Native builds complete" build-js: @echo "Building JavaScript frameworks..." cd $(FRAMEWORKS_DIR) && npm install @echo "✓ JavaScript frameworks ready" clean: cd $(PROJECT_ROOT) && opam exec -- dune clean rm -rf $(RESULTS_DIR)/*.json $(RESULTS_DIR)/*.md rm -rf $(FRAMEWORKS_DIR)/node_modules @echo "✓ Cleaned" # ============================================================================ # Benchmark Targets # ============================================================================ bench: bench-render bench-memory bench-streaming @echo "✓ All benchmarks complete" bench-memory: build-native @echo "\n=== Running Memory Benchmarks ===" $(PROJECT_ROOT)/_build/default/benchmark/memory/memory_bench.exe bench-memory-json: build-native @mkdir -p $(RESULTS_DIR) $(PROJECT_ROOT)/_build/default/benchmark/memory/memory_bench.exe --json > $(RESULTS_DIR)/memory-$$(date +%Y%m%d-%H%M%S).json bench-streaming: build-native @echo "\n=== Running Streaming Benchmarks ===" $(PROJECT_ROOT)/_build/default/benchmark/streaming/streaming_bench.exe bench-render: build-native build-js @echo "\n=== Pure Render Benchmark: Native vs Node vs Bun ===" @echo "\n--- Native (server-reason-react) ---" $(PROJECT_ROOT)/_build/default/benchmark/streaming/streaming_bench.exe @echo "\n--- Node (React, NODE_ENV=production) ---" @cd $(FRAMEWORKS_DIR) && ./node_modules/.bin/esbuild render-bench.ts --bundle --outfile=.render-bench-node.mjs --format=esm --platform=node --external:react --external:react-dom --loader:.jsx=jsx --loader:.ts=ts > /dev/null && NODE_ENV=production node .render-bench-node.mjs; rm -f $(FRAMEWORKS_DIR)/.render-bench-node.mjs @echo "\n--- Bun (React, NODE_ENV=production) ---" @cd $(FRAMEWORKS_DIR) && NODE_ENV=production $(BUN) render-bench.ts bench-render-native: build-native @echo "\n=== Native Render Benchmark ===" $(PROJECT_ROOT)/_build/default/benchmark/streaming/streaming_bench.exe bench-render-bun: build-js @echo "\n=== Bun Render Benchmark (NODE_ENV=production) ===" @cd $(FRAMEWORKS_DIR) && NODE_ENV=production $(BUN) render-bench.ts bench-render-node: build-js @echo "\n=== Node Render Benchmark (NODE_ENV=production) ===" @cd $(FRAMEWORKS_DIR) && ./node_modules/.bin/esbuild render-bench.ts --bundle --outfile=.render-bench-node.mjs --format=esm --platform=node --external:react --external:react-dom --loader:.jsx=jsx --loader:.ts=ts > /dev/null && NODE_ENV=production node .render-bench-node.mjs; rm -f $(FRAMEWORKS_DIR)/.render-bench-node.mjs bench-http: build @echo "\n=== Running HTTP Benchmarks ===" @mkdir -p $(RESULTS_DIR) cd $(BENCHMARK_DIR)/runner && node runner.mjs bench-http-quick: build @echo "\n=== Running Quick HTTP Benchmarks ===" cd $(BENCHMARK_DIR)/runner && node runner.mjs --scenarios trivial,table100 bench-native-http: build-native @echo "\n=== Running Native HTTP Benchmarks ===" cd $(BENCHMARK_DIR)/runner && node runner.mjs --frameworks dream-native bench-js-http: build-js @echo "\n=== Running JavaScript HTTP Benchmarks ===" cd $(BENCHMARK_DIR)/runner && node runner.mjs --frameworks node-express,node-fastify,hono-node # ============================================================================ # Server Targets (for manual testing) # ============================================================================ native-server: build-native @echo "Starting native Dream server on port 3000..." PORT=3000 $(PROJECT_ROOT)/_build/default/benchmark/native/server.exe js-servers: build-js @echo "Starting all JavaScript servers..." cd $(FRAMEWORKS_DIR) && npm run start:all node-express: build-js cd $(FRAMEWORKS_DIR) && npm run start:node-express node-fastify: build-js cd $(FRAMEWORKS_DIR) && npm run start:node-fastify hono-node: build-js cd $(FRAMEWORKS_DIR) && npm run start:hono-node # ============================================================================ # Utility Targets # ============================================================================ wrk-test: @if [ -z "$(PORT)" ]; then \ echo "Usage: make wrk-test PORT=3000 [SCENARIO=table100]"; \ else \ wrk -t4 -c100 -d10s "http://localhost:$(PORT)/?scenario=$${SCENARIO:-table100}"; \ fi scenarios: build-native @$(PROJECT_ROOT)/_build/default/benchmark/native/server.exe & @sleep 1 @curl -s http://localhost:3000/scenarios | jq . @pkill -f "server.exe" || true results: @ls -la $(RESULTS_DIR)/*.md $(RESULTS_DIR)/*.json 2>/dev/null || echo "No results yet. Run 'make bench-http'" # ============================================================================ # Help # ============================================================================ help: @echo "Server Reason React Benchmark Suite" @echo "====================================" @echo "" @echo "Build targets:" @echo " make build - Build all (native + JS)" @echo " make build-native - Build native OCaml benchmarks" @echo " make build-js - Install JS framework dependencies" @echo " make clean - Clean all build artifacts" @echo "" @echo "Benchmark targets:" @echo " make bench - Run all benchmarks" @echo " make bench-render - Pure render comparison: Native vs Node vs Bun (recommended)" @echo " make bench-render-native - Native render only" @echo " make bench-render-node - Node render only (NODE_ENV=production)" @echo " make bench-render-bun - Bun render only (NODE_ENV=production)" @echo " make bench-memory - Run memory benchmarks" @echo " make bench-streaming - Run streaming benchmarks" @echo " make bench-http - Run HTTP load testing (all frameworks)" @echo " make bench-http-quick - Quick HTTP test (trivial + table100 only)" @echo " make bench-native-http - HTTP test native only" @echo " make bench-js-http - HTTP test JS frameworks only" @echo "" @echo "Server targets (for manual testing):" @echo " make native-server - Start native Dream server (port 3000)" @echo " make js-servers - Start all JS servers" @echo " make node-express - Start Node.js + Express" @echo " make node-fastify - Start Node.js + Fastify" @echo " make hono-node - Start Hono + Node.js" @echo "" @echo "Utility:" @echo " make wrk-test PORT=3000 [SCENARIO=table100]" @echo " - Quick load test against running server" @echo " make scenarios - List available scenarios" @echo " make results - Show benchmark results" @echo "" @echo "Optimization control:" @echo " DISABLE_OPTIMIZATIONS=1 make build-native" @echo " - Build without optimizations (--profile=dev)" @echo " make build-native - Build with optimizations (--profile=release, default)" ================================================ FILE: benchmark/README.md ================================================ # Server Reason React Benchmark Suite A comprehensive benchmark suite for measuring and comparing SSR performance of `server-reason-react` against React.js on JavaScript runtimes. ## Performance Summary Pure rendering performance — `server-reason-react` (release profile, flambda + PPX static analyzer enabled) vs React 19.2.5 on Node 22.22.0 and Bun 1.3.12, both with `NODE_ENV=production`. Numbers are mean of 100 iterations, `renderToString`. All 17 scenarios the native bench exercises are mirrored in JS, so every row is a like-for-like comparison of the same component tree rendered by three different runtimes. | Scenario | SRR | Node + React | Bun + React | SRR vs Node | SRR vs Bun | |-------------|------------:|-------------:|------------:|------------:|-----------:| | Trivial | **0.29 µs** | 42.10 µs | 44.54 µs | **145x** | **154x** | | ShallowTree | **16.78 µs**| 99.97 µs | 101.17 µs | **6.0x** | **6.0x** | | DeepTree10 | **13.74 µs**| 35.93 µs | 35.97 µs | **2.6x** | **2.6x** | | DeepTree50 | **67.07 µs**| 91.77 µs | 105.36 µs | **1.4x** | **1.6x** | | WideTree10 | **14.78 µs**| 66.20 µs | 96.20 µs | **4.5x** | **6.5x** | | WideTree100 | **216.24 µs**| 382.75 µs | 346.96 µs | **1.8x** | **1.6x** | | WideTree500 | **1.15 ms** | 2.30 ms | 1.55 ms | **2.0x** | **1.3x** | | Table10 | **35.95 µs**| 219.12 µs | 200.06 µs | **6.1x** | **5.6x** | | Table100 | **301.89 µs**| 714.19 µs | 628.45 µs | **2.4x** | **2.1x** | | Table500 | **1.35 ms** | 4.97 ms | 3.05 ms | **3.7x** | **2.3x** | | PropsSmall | **107.42 µs**| 217.50 µs | 204.00 µs | **2.0x** | **1.9x** | | PropsMedium | **305.64 µs**| 395.08 µs | 361.21 µs | **1.3x** | **1.2x** | | Ecommerce24 | **121.24 µs**| 373.23 µs | 376.76 µs | **3.1x** | **3.1x** | | Ecommerce48 | **228.68 µs**| 488.66 µs | 492.02 µs | **2.1x** | **2.2x** | | Dashboard | **31.68 µs**| 85.82 µs | 94.09 µs | **2.7x** | **3.0x** | | Blog50 | **182.86 µs**| 565.45 µs | 587.69 µs | **3.1x** | **3.2x** | | Form | **69.39 µs**| 206.54 µs | 163.92 µs | **3.0x** | **2.4x** | Throughput (MB/s, higher is better): | Scenario | SRR | Node | Bun | |-------------|----:|-----:|----:| | Table500 | **505.2** | 137.2 | 223.6 | | Blog50 | **500.8** | 161.9 | 155.8 | | Table100 | **455.4** | 192.5 | 218.8 | | Dashboard | **440.2** | 162.5 | 148.2 | | WideTree10 | **439.2** | 98.1 | 67.5 | | Ecommerce24 | **427.3** | 138.8 | 137.5 | | Table10 | **421.1** | 69.1 | 75.7 | | Ecommerce48 | **410.6** | 192.1 | 190.8 | | Form | **310.6** | 104.4 | 131.5 | | PropsMedium | **302.6** | 234.1 | 256.0 | | WideTree500 | **282.3** | 141.1 | 209.4 | Notes: - **SRR wins on every scenario.** Typical margin is 1.3–3.1x on realistic pages; the largest wins are on Table10 (5.6–6.1x), Table500 (2.3–3.7x), ShallowTree (6.0x), and WideTree10 (4.5–6.5x). Trivial's ~145x is measurement floor noise, not a real rendering difference. - **Bun beats Node** on large wide trees and tables (1.3–1.6x), but loses on attribute-heavy and small-tree scenarios. On most real-page scenarios the two are within 10% of each other. - React 19 `renderToString` is measurably slower than 18.3.1 on small scenarios (Trivial went from ~19 µs → ~42 µs) because of added server-component machinery that runs on every render. Large scenarios are roughly on par or slightly faster. > These benchmarks measure pure `renderToStaticMarkup`/`renderToString` performance without HTTP server overhead. JS runs require `NODE_ENV=production`; the harness refuses to run otherwise (React's dev build is 3–7x slower and would give meaningless numbers). ## Quick Start ```bash # Build everything make build # Run all benchmarks make bench # Run the render comparision make bench-render ``` ## Optimization Control By default, benchmarks are built with optimizations enabled (`--profile=release`). You can disable optimizations to compare performance: ```bash # Build without optimizations DISABLE_OPTIMIZATIONS=1 make build-native # Run benchmarks without optimizations DISABLE_OPTIMIZATIONS=1 make bench-render # Re-enable optimizations (default) make build-native make bench-render ``` This is useful for: - Comparing optimized vs unoptimized performance - CI workflows that need to test both configurations - Debugging performance differences ## Benchmark Categories ### 1. Render Comparison (`bench-render`) **The official benchmark** — compares pure rendering performance without HTTP overhead: ```bash make bench-render # Native vs Bun side-by-side make bench-render-native # Native only make bench-render-bun # Bun only ``` This measures `renderToStaticMarkup` (native) vs `renderToString` (Bun/React) directly, giving the most accurate comparison of SSR performance. ### 2. Memory Benchmarks (`bench-memory`) Measures allocation and GC behavior: - Words allocated per render - Minor/major GC cycles - Memory efficiency (output bytes per word) ```bash make bench-memory make bench-memory-json # Output JSON for CI ``` ### 3. Streaming Benchmarks (`bench-streaming`) Compares `renderToStaticMarkup` vs `renderToString` on native: - Time to render - Throughput (MB/s) - Overhead comparison ```bash make bench-streaming ``` ### 4. HTTP Load Testing (`bench-http`) Full end-to-end HTTP benchmarks using wrk: - Requests/second - Latency distribution (avg, p99) - Transfer rate - Multi-framework comparison ```bash make bench-http # Full suite, all frameworks make bench-http-quick # Quick test (trivial + table100) make bench-native-http # Native only make bench-js-http # JavaScript frameworks only ``` ## Test Scenarios | Scenario | Description | Purpose | |----------|-------------|---------| | `trivial` | Hello world | Baseline overhead | | `shallow` | 5-level component tree | Prop passing | | `deep10-100` | 10-100 level deep tree | Recursion performance | | `wide10-1000` | 10-1000 siblings | Array rendering | | `table10-500` | Data table rows | Real-world tables | | `props_*` | Many HTML attributes | Attribute serialization | | `ecommerce24-100` | Product grid | E-commerce SSR | | `dashboard` | Analytics page | Admin UI | | `blog10-100` | Article + comments | Content-heavy | | `form` | Multi-step form | Form rendering | ## Framework Comparison | Framework | Port | Description | |-----------|------|-------------| | `dream-native` | 3000 | OCaml + Dream + server-reason-react | | `node-express` | 3001 | Node.js + Express + React | | `node-fastify` | 3002 | Node.js + Fastify + React | | `hono-node` | 3003 | Hono + Node.js + React | | `hono-bun` | 3004 | Hono + Bun + React | | `bun-native` | 3005 | Bun native + React | | `preact` | 3006 | Node.js + Express + Preact | ## Running Individual Servers For manual testing or debugging: ```bash # Start servers individually make native-server # Dream on :3000 make node-express # Express on :3001 make node-fastify # Fastify on :3002 make hono-node # Hono on :3003 DISABLE_LOGGER=1 make native-server # Test with curl curl "http://localhost:3000/?scenario=table100" curl "http://localhost:3000/scenarios" # List available # Quick wrk test make wrk-test PORT=3000 SCENARIO=table100 ``` ## Methodology ### Warmup - 100 requests before measurement to eliminate cold-start effects - GC stabilization between benchmark runs ### Measurement - Multiple iterations with statistical analysis - Core_bench: 10,000 calls per benchmark - HTTP: 10-30 seconds with wrk (configurable) ### Environment - All frameworks run single-threaded for fair comparison - React runtimes require `NODE_ENV=production` — `render-bench.ts` refuses to run otherwise, and `make bench-render` sets it automatically. React's development build skips a large amount of dev-only validation in production and is 3–7x faster as a result. - Same React/component tree across frameworks ### Metrics Reported - **Throughput**: Requests/second - **Latency**: Average, p50, p99, max - **Memory**: Words allocated, GC cycles - **Transfer**: Bytes/second ## Requirements - OCaml 5.x with opam - Node.js 20+ (for JS frameworks) - Bun (optional, for Bun benchmarks) - wrk (optional, for HTTP load testing) ## Results Results are saved to `benchmark/results/`: - `benchmark-TIMESTAMP.json` - Raw data - `benchmark-TIMESTAMP.md` - Markdown report View latest results: ```bash make results ``` ## Interpreting Results ### What to look for: 1. **Render time (µs)**: Lower is better. Time to render a component tree. 2. **Throughput (MB/s)**: Higher is better. Data output rate. 3. **Memory (words/iter)**: Lower is better. Memory efficiency. 4. **Requests/sec** (HTTP): Higher is better. End-to-end throughput. ### Typical results pattern: **Pure rendering** (no HTTP, React runtimes in `NODE_ENV=production`): - SRR is **1.3–6.1x faster** than Node + React and **1.2–6.5x faster** than Bun + React across all 17 scenarios. The biggest realistic wins are on tables (up to 6.1x on Table10 vs Node) and shallow/wide component trees (4.5–6.5x on WideTree10). - SRR throughput tops out around **505 MB/s** on tables and **500 MB/s** on content pages (Blog50). Bun is competitive on a handful of scenarios (~225 MB/s on Table500); Node trails at ~100–195 MB/s on large scenarios. - The "trivial" scenario shows a ~145x gap that's mostly measurement floor noise, not a real rendering difference. - Node and Bun are roughly at parity on most real-page scenarios. Bun wins on large wide trees and tables; Node wins on attribute-heavy and small-tree scenarios. **Inline `style` props are expensive in SRR.** `ReactDOM.Style.make` has ~347 optional args and allocates ~1,460 words per call regardless of what's passed. If a component is on a hot render path and uses inline `style`, consider moving the style into a CSS class. Scenarios in this suite use `className` so the measurement reflects the rendering path, not `Style.make` allocation. **HTTP benchmarks** (with server overhead): - Bun's HTTP layer is faster for minimal requests (trivial scenario) - Native wins on real SSR workloads where rendering dominates - Use `DISABLE_LOGGER=1` for accurate native HTTP benchmarks ## Contributing To add a new scenario: 1. Create `benchmark/scenarios/NewScenario.re` 2. Add to the scenario list in: - `benchmark/native/server.re` - `benchmark/micro/micro_bench.re` - `benchmark/frameworks/shared/scenarios.jsx` 3. Run benchmarks: `make bench` ================================================ FILE: benchmark/allocation.ml ================================================ let measure_alloc title f = let before = Gc.stat () in let result = f () in let after = Gc.stat () in Printf.printf "\n=== %s ===\n" title; Printf.printf "Minor words: %f\n" (after.minor_words -. before.minor_words); Printf.printf "Major words: %f\n" (after.major_words -. before.major_words); Printf.printf "Minor collections: %d\n" (after.minor_collections - before.minor_collections); result let loop n f = for _ = 1 to n do f () done let main () = let filter_map_style () = let element = React.createElement "div" (Stdlib.List.filter_map Fun.id (List.init 50 (fun i -> Some (React.JSX.String (Printf.sprintf "prop%d" i, Printf.sprintf "prop%d" i, Printf.sprintf "value%d" i))))) [] in let _ = ReactDOM.renderToStaticMarkup element in () in let direct_style () = let props = List.init 50 (fun i -> React.JSX.String (Printf.sprintf "prop%d" i, Printf.sprintf "prop%d" i, Printf.sprintf "value%d" i)) in let element = React.createElement "div" props [] in let _ = ReactDOM.renderToStaticMarkup element in () in let render_hello_world () = let _ = ReactDOM.renderToStaticMarkup (Static_small.make (Static_small.makeProps ())) in () in let render_app () = let _ = ReactDOM.renderToStaticMarkup (App.make (App.makeProps ())) in () in let render_React_list ~num () = let list = React.list (List.init num (fun i -> React.string (Printf.sprintf "index: %d" i))) in let _ = ReactDOM.renderToStaticMarkup (React.createElement "div" [] [ list ]) in () in let render_React_array ~num () = let array = React.array (Array.init num (fun i -> React.string (Printf.sprintf "index: %d" i))) in let _ = ReactDOM.renderToStaticMarkup (React.createElement "div" [] [ array ]) in () in measure_alloc "Use filter_map" (fun () -> loop 10000 filter_map_style); measure_alloc "Use list direct style" (fun () -> loop 10000 direct_style); measure_alloc "Render " (fun () -> loop 10000 render_hello_world); measure_alloc "Render " (fun () -> loop 10000 render_app); measure_alloc "Render React.list 10" (fun () -> loop 10000 (render_React_list ~num:10)); measure_alloc "Render React.array 10" (fun () -> loop 10000 (render_React_array ~num:10)); measure_alloc "Render React.list 100" (fun () -> loop 10000 (render_React_list ~num:100)); measure_alloc "Render React.array 100" (fun () -> loop 10000 (render_React_array ~num:100)); (* measure_alloc "Render React.list 1000" (fun () -> loop 10000 (render_React_list ~num:1000)); *) (* measure_alloc "Render React.array 1000" (fun () -> loop 10000 (render_React_array ~num:1000)); *) Lwt.return () let () = Lwt_main.run (main ()) ================================================ FILE: benchmark/bench.ml ================================================ (* Benchmark runner with JSON output for github-action-benchmark (customBiggerIsBetter) *) let json_mode = ref false let iterations = 10000 type benchmark_result = { name : string; ops_per_sec : float } let measure_benchmark ~name render_fn = for _ = 1 to 100 do let _ = render_fn () in () done; Gc.full_major (); Gc.compact (); let start = Unix.gettimeofday () in for _ = 1 to iterations do let _ = render_fn () in () done; let elapsed = Unix.gettimeofday () -. start in let ops_per_sec = float_of_int iterations /. elapsed in { name; ops_per_sec } let lwt_iterations = 1000 let measure_benchmark_lwt ~name render_fn = for _ = 1 to 10 do let _ = Lwt_main.run (render_fn ()) in () done; Gc.full_major (); Gc.compact (); let start = Unix.gettimeofday () in for _ = 1 to lwt_iterations do let _ = Lwt_main.run (render_fn ()) in () done; let elapsed = Unix.gettimeofday () -. start in let ops_per_sec = float_of_int lwt_iterations /. elapsed in { name; ops_per_sec } let print_result r = Printf.printf "%-35s %12.0f ops/sec\n" r.name r.ops_per_sec let print_results_table results = Printf.printf "\n%s\n" (String.make 50 '='); Printf.printf "server-reason-react Benchmarks\n"; Printf.printf "%s\n" (String.make 50 '='); Printf.printf "%-35s %12s\n" "Benchmark" "Throughput"; Printf.printf "%s\n" (String.make 50 '-'); List.iter print_result results; Printf.printf "%s\n" (String.make 50 '=') let print_results_json results = let json_entries = List.map (fun r -> Printf.sprintf {| {"name": "%s", "unit": "ops/sec", "value": %.2f}|} r.name r.ops_per_sec) results in print_endline "["; print_endline (String.concat ",\n" json_entries); print_endline "]" let () = let args = Array.to_list Sys.argv in json_mode := List.mem "--json" args; let open Benchmark_scenarios in let results = [ measure_benchmark ~name:"trivial/renderToStaticMarkup" (fun () -> ReactDOM.renderToStaticMarkup (Trivial.make (Trivial.makeProps ()))); measure_benchmark ~name:"trivial/renderToString" (fun () -> ReactDOM.renderToString (Trivial.make (Trivial.makeProps ()))); measure_benchmark ~name:"depth/10" (fun () -> ReactDOM.renderToStaticMarkup (DeepTree.Depth10.make (DeepTree.Depth10.makeProps ()))); measure_benchmark ~name:"depth/25" (fun () -> ReactDOM.renderToStaticMarkup (DeepTree.Depth25.make (DeepTree.Depth25.makeProps ()))); measure_benchmark ~name:"depth/50" (fun () -> ReactDOM.renderToStaticMarkup (DeepTree.Depth50.make (DeepTree.Depth50.makeProps ()))); measure_benchmark ~name:"depth/100" (fun () -> ReactDOM.renderToStaticMarkup (DeepTree.Depth100.make (DeepTree.Depth100.makeProps ()))); measure_benchmark ~name:"width/10" (fun () -> ReactDOM.renderToStaticMarkup (WideTree.Wide10.make (WideTree.Wide10.makeProps ()))); measure_benchmark ~name:"width/100" (fun () -> ReactDOM.renderToStaticMarkup (WideTree.Wide100.make (WideTree.Wide100.makeProps ()))); measure_benchmark ~name:"width/500" (fun () -> ReactDOM.renderToStaticMarkup (WideTree.Wide500.make (WideTree.Wide500.makeProps ()))); measure_benchmark ~name:"width/1000" (fun () -> ReactDOM.renderToStaticMarkup (WideTree.Wide1000.make (WideTree.Wide1000.makeProps ()))); measure_benchmark ~name:"table/10" (fun () -> ReactDOM.renderToStaticMarkup (Table.Table10.make (Table.Table10.makeProps ()))); measure_benchmark ~name:"table/50" (fun () -> ReactDOM.renderToStaticMarkup (Table.Table50.make (Table.Table50.makeProps ()))); measure_benchmark ~name:"table/100" (fun () -> ReactDOM.renderToStaticMarkup (Table.Table100.make (Table.Table100.makeProps ()))); measure_benchmark ~name:"table/500" (fun () -> ReactDOM.renderToStaticMarkup (Table.Table500.make (Table.Table500.makeProps ()))); measure_benchmark ~name:"props/small" (fun () -> ReactDOM.renderToStaticMarkup (PropsHeavy.Small.make (PropsHeavy.Small.makeProps ()))); measure_benchmark ~name:"props/medium" (fun () -> ReactDOM.renderToStaticMarkup (PropsHeavy.Medium.make (PropsHeavy.Medium.makeProps ()))); measure_benchmark ~name:"props/large" (fun () -> ReactDOM.renderToStaticMarkup (PropsHeavy.Large.make (PropsHeavy.Large.makeProps ()))); measure_benchmark ~name:"realworld/ecommerce24" (fun () -> ReactDOM.renderToStaticMarkup (Ecommerce.Products24.make (Ecommerce.Products24.makeProps ()))); measure_benchmark ~name:"realworld/ecommerce48" (fun () -> ReactDOM.renderToStaticMarkup (Ecommerce.Products48.make (Ecommerce.Products48.makeProps ()))); measure_benchmark ~name:"realworld/dashboard" (fun () -> ReactDOM.renderToStaticMarkup (Dashboard.make (Dashboard.makeProps ()))); measure_benchmark ~name:"realworld/blog50" (fun () -> ReactDOM.renderToStaticMarkup (Blog.Blog50.make (Blog.Blog50.makeProps ()))); measure_benchmark ~name:"realworld/form" (fun () -> ReactDOM.renderToStaticMarkup (Form.make (Form.makeProps ()))); measure_benchmark ~name:"primitive/React.string" (fun () -> ReactDOM.renderToStaticMarkup (React.string "Hello")); measure_benchmark ~name:"primitive/React.int" (fun () -> ReactDOM.renderToStaticMarkup (React.int 42)); measure_benchmark ~name:"primitive/React.null" (fun () -> ReactDOM.renderToStaticMarkup React.null); measure_benchmark ~name:"primitive/createElement_empty" (fun () -> ReactDOM.renderToStaticMarkup (React.createElement "div" [] [])); measure_benchmark ~name:"primitive/createElement_children" (fun () -> let children = List.init 10 (fun i -> React.string (string_of_int i)) in ReactDOM.renderToStaticMarkup (React.createElement "div" [] children)); measure_benchmark ~name:"primitive/React.array_10" (fun () -> let arr = Array.init 10 (fun i -> React.string (string_of_int i)) in ReactDOM.renderToStaticMarkup (React.createElement "div" [] [ React.array arr ])); measure_benchmark ~name:"primitive/React.array_100" (fun () -> let arr = Array.init 100 (fun i -> React.string (string_of_int i)) in ReactDOM.renderToStaticMarkup (React.createElement "div" [] [ React.array arr ])); measure_benchmark ~name:"primitive/React.list_10" (fun () -> let lst = List.init 10 (fun i -> React.string (string_of_int i)) in ReactDOM.renderToStaticMarkup (React.createElement "div" [] [ React.list lst ])); measure_benchmark ~name:"primitive/React.list_100" (fun () -> let lst = List.init 100 (fun i -> React.string (string_of_int i)) in ReactDOM.renderToStaticMarkup (React.createElement "div" [] [ React.list lst ])); measure_benchmark_lwt ~name:"rsc/trivial" (fun () -> let%lwt html, _subscribe = ReactServerDOM.render_html (Trivial.make (Trivial.makeProps ())) in Lwt.return html); measure_benchmark_lwt ~name:"rsc/depth/50" (fun () -> let%lwt html, _subscribe = ReactServerDOM.render_html (DeepTree.Depth50.make (DeepTree.Depth50.makeProps ())) in Lwt.return html); measure_benchmark_lwt ~name:"rsc/width/100" (fun () -> let%lwt html, _subscribe = ReactServerDOM.render_html (WideTree.Wide100.make (WideTree.Wide100.makeProps ())) in Lwt.return html); measure_benchmark_lwt ~name:"rsc/width/500" (fun () -> let%lwt html, _subscribe = ReactServerDOM.render_html (WideTree.Wide500.make (WideTree.Wide500.makeProps ())) in Lwt.return html); measure_benchmark_lwt ~name:"rsc/width/1000" (fun () -> let%lwt html, _subscribe = ReactServerDOM.render_html (WideTree.Wide1000.make (WideTree.Wide1000.makeProps ())) in Lwt.return html); measure_benchmark_lwt ~name:"rsc/table/100" (fun () -> let%lwt html, _subscribe = ReactServerDOM.render_html (Table.Table100.make (Table.Table100.makeProps ())) in Lwt.return html); measure_benchmark_lwt ~name:"rsc/table/500" (fun () -> let%lwt html, _subscribe = ReactServerDOM.render_html (Table.Table500.make (Table.Table500.makeProps ())) in Lwt.return html); ] in if !json_mode then print_results_json results else print_results_table results ================================================ FILE: benchmark/dune ================================================ (executable (name bench) (modules bench) (libraries unix lwt lwt.unix benchmark_scenarios server-reason-react.js server-reason-react.react server-reason-react.reactDom) (preprocess (pps server-reason-react.ppx lwt_ppx))) (executable (name allocation) (modules allocation) (libraries unix lwt lwt.unix server-reason-react.js server-reason-react.react server-reason-react.reactDom demo_shared_native) (preprocess (pps server-reason-react.ppx))) (rule (alias bench) (action (run ./bench.exe))) (rule (alias bench-json) (action (run ./bench.exe --json))) ================================================ FILE: benchmark/frameworks/bun-native/server.tsx ================================================ /** * Bun Native + React SSR Benchmark Server */ import React from "react"; import ReactDOMServer from "react-dom/server"; import * as scenarios from "../shared/scenarios.jsx"; const PORT = Number(process.env.PORT) || 3005; Bun.serve({ port: PORT, fetch(req) { const url = new URL(req.url); if (url.pathname === "/health") { return Response.json({ status: "ok", framework: "bun-native", pid: process.pid, }); } if (url.pathname === "/scenarios") { const list = Object.entries(scenarios.scenarios).map(([key, val]) => ({ key, name: val.name, description: val.description, })); return Response.json(list); } if (url.pathname === "/") { const scenarioName = url.searchParams.get("scenario") || "table100"; const scenario = scenarios.scenarios[scenarioName as keyof typeof scenarios.scenarios]; if (!scenario) { return new Response(`Unknown scenario: ${scenarioName}`, { status: 404 }); } const Component = scenario.component; const html = ReactDOMServer.renderToString(React.createElement(Component)); return new Response( `Benchmark
${html}
`, { headers: { "Content-Type": "text/html" }, } ); } return new Response("Not Found", { status: 404 }); }, }); console.log(`[bun-native] Server listening on http://localhost:${PORT}`); console.log(`[bun-native] PID: ${process.pid}`); ================================================ FILE: benchmark/frameworks/hono-bun/server.ts ================================================ /** * Hono + Bun + React SSR Benchmark Server */ import { Hono } from "hono"; import React from "react"; import ReactDOMServer from "react-dom/server"; import * as scenarios from "../shared/scenarios.jsx"; const app = new Hono(); const PORT = Number(process.env.PORT) || 3004; // Scenario selection via query param app.get("/", (c) => { const scenarioName = c.req.query("scenario") || "table100"; const scenario = scenarios.scenarios[scenarioName as keyof typeof scenarios.scenarios]; if (!scenario) { c.status(404); return c.text(`Unknown scenario: ${scenarioName}`); } const Component = scenario.component; const html = ReactDOMServer.renderToString(React.createElement(Component)); return c.html( `Benchmark
${html}
` ); }); // Health check app.get("/health", (c) => { return c.json({ status: "ok", framework: "hono-bun", pid: process.pid }); }); // List available scenarios app.get("/scenarios", (c) => { const list = Object.entries(scenarios.scenarios).map(([key, val]) => ({ key, name: val.name, description: val.description, })); return c.json(list); }); export default { port: PORT, fetch: app.fetch, }; console.log(`[hono-bun] Server listening on http://localhost:${PORT}`); console.log(`[hono-bun] PID: ${process.pid}`); ================================================ FILE: benchmark/frameworks/hono-node/server.mjs ================================================ /** * Hono + Node.js + React SSR Benchmark Server */ import { serve } from "@hono/node-server"; import { Hono } from "hono"; import React from "react"; import ReactDOMServer from "react-dom/server"; import * as scenarios from "../shared/scenarios.jsx"; const app = new Hono(); const PORT = process.env.PORT || 3003; // Scenario selection via query param app.get("/", (c) => { const scenarioName = c.req.query("scenario") || "table100"; const scenario = scenarios.scenarios[scenarioName]; if (!scenario) { c.status(404); return c.text(`Unknown scenario: ${scenarioName}`); } const Component = scenario.component; const html = ReactDOMServer.renderToString(React.createElement(Component)); return c.html( `Benchmark
${html}
` ); }); // Health check app.get("/health", (c) => { return c.json({ status: "ok", framework: "hono-node", pid: process.pid }); }); // List available scenarios app.get("/scenarios", (c) => { const list = Object.entries(scenarios.scenarios).map(([key, val]) => ({ key, name: val.name, description: val.description, })); return c.json(list); }); serve( { fetch: app.fetch, port: PORT, }, (info) => { console.log(`[hono-node] Server listening on http://localhost:${info.port}`); console.log(`[hono-node] PID: ${process.pid}`); } ); ================================================ FILE: benchmark/frameworks/node-express/server.mjs ================================================ /** * Node.js + Express + React SSR Benchmark Server */ import express from "express"; import React from "react"; import ReactDOMServer from "react-dom/server"; import * as scenarios from "../shared/scenarios.jsx"; const app = express(); const PORT = process.env.PORT || 3001; // Scenario selection via query param app.get("/", (req, res) => { const scenarioName = req.query.scenario || "table100"; const scenario = scenarios.scenarios[scenarioName]; if (!scenario) { return res.status(404).send(`Unknown scenario: ${scenarioName}`); } const Component = scenario.component; const html = ReactDOMServer.renderToString(React.createElement(Component)); res.setHeader("Content-Type", "text/html"); res.send(`Benchmark
${html}
`); }); // Health check app.get("/health", (req, res) => { res.json({ status: "ok", framework: "node-express", pid: process.pid }); }); // List available scenarios app.get("/scenarios", (req, res) => { const list = Object.entries(scenarios.scenarios).map(([key, val]) => ({ key, name: val.name, description: val.description, })); res.json(list); }); app.listen(PORT, () => { console.log(`[node-express] Server listening on http://localhost:${PORT}`); console.log(`[node-express] PID: ${process.pid}`); }); ================================================ FILE: benchmark/frameworks/node-fastify/server.mjs ================================================ /** * Node.js + Fastify + React SSR Benchmark Server */ import Fastify from "fastify"; import React from "react"; import ReactDOMServer from "react-dom/server"; import * as scenarios from "../shared/scenarios.jsx"; const fastify = Fastify({ logger: false, }); const PORT = process.env.PORT || 3002; // Scenario selection via query param fastify.get("/", async (request, reply) => { const scenarioName = request.query.scenario || "table100"; const scenario = scenarios.scenarios[scenarioName]; if (!scenario) { reply.code(404); return `Unknown scenario: ${scenarioName}`; } const Component = scenario.component; const html = ReactDOMServer.renderToString(React.createElement(Component)); reply.type("text/html"); return `Benchmark
${html}
`; }); // Health check fastify.get("/health", async () => { return { status: "ok", framework: "node-fastify", pid: process.pid }; }); // List available scenarios fastify.get("/scenarios", async () => { return Object.entries(scenarios.scenarios).map(([key, val]) => ({ key, name: val.name, description: val.description, })); }); const start = async () => { try { await fastify.listen({ port: PORT, host: "0.0.0.0" }); console.log(`[node-fastify] Server listening on http://localhost:${PORT}`); console.log(`[node-fastify] PID: ${process.pid}`); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); ================================================ FILE: benchmark/frameworks/package.json ================================================ { "name": "benchmark-frameworks", "version": "1.0.0", "private": true, "type": "module", "scripts": { "build:shared": "esbuild shared/App.jsx --bundle --outfile=dist/App.js --format=cjs --platform=node --external:react --external:react-dom", "start:node-express": "NODE_ENV=production node node-express/server.mjs", "start:node-fastify": "NODE_ENV=production node node-fastify/server.mjs", "start:hono-node": "NODE_ENV=production node hono-node/server.mjs", "start:hono-bun": "NODE_ENV=production bun hono-bun/server.ts", "start:bun": "NODE_ENV=production bun bun-native/server.tsx", "start:preact": "NODE_ENV=production node preact/server.mjs", "start:all": "concurrently -n 'express,fastify,hono-node,preact' 'npm run start:node-express' 'npm run start:node-fastify' 'npm run start:hono-node' 'npm run start:preact'" }, "dependencies": { "@hono/node-server": "^1.13.1", "express": "^4.21.0", "fastify": "^5.0.0", "hono": "^4.6.3", "preact": "^10.24.1", "preact-render-to-string": "^6.5.10", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { "@types/bun": "^1.1.10", "@types/node": "^22.7.4", "concurrently": "^9.0.1", "esbuild": "^0.24.0", "typescript": "^5.6.2" } } ================================================ FILE: benchmark/frameworks/preact/server.mjs ================================================ /** * Node.js + Express + Preact SSR Benchmark Server * Tests Preact's lighter-weight alternative to React */ import express from "express"; import { h } from "preact"; import renderToString from "preact-render-to-string"; const app = express(); const PORT = process.env.PORT || 3006; // ============================================================================ // Preact-native scenarios (mirrors React scenarios) // ============================================================================ const Trivial = () => h("div", null, "Hello World"); // ShallowTree const Level5 = ({ title, subtitle, active, count }) => h( "div", { class: `p-4 rounded ${active ? "bg-blue-500" : ""}` }, h("h5", { class: "font-bold" }, title), h("p", { class: "text-sm text-gray-500" }, subtitle), h("span", { class: "badge" }, count) ); const Level4 = ({ title, description, isHighlighted, itemCount }) => h( "section", { class: `mb-4 ${isHighlighted ? "border-l-4 border-blue-500" : ""}` }, h(Level5, { title, subtitle: description, active: isHighlighted, count: itemCount }), h(Level5, { title: `${title} Alt`, subtitle: "Secondary", active: false, count: itemCount * 2 }) ); const Level3 = ({ groupName, expanded, totalItems }) => h( "article", { class: `p-6 ${expanded ? "shadow-lg" : ""}` }, h("h3", { class: "text-xl font-semibold mb-4" }, groupName), h(Level4, { title: "First Item", description: "Description A", isHighlighted: true, itemCount: totalItems }), h(Level4, { title: "Second Item", description: "Description B", isHighlighted: false, itemCount: Math.floor(totalItems / 2) }) ); const Level2 = ({ sectionTitle, isVisible }) => h( "div", { class: `container mx-auto ${isVisible ? "block" : ""}` }, h("h2", { class: "text-2xl font-bold mb-6" }, sectionTitle), h(Level3, { groupName: "Group Alpha", expanded: true, totalItems: 42 }), h(Level3, { groupName: "Group Beta", expanded: false, totalItems: 17 }) ); const Level1 = ({ pageTitle }) => h( "main", { class: "min-h-screen bg-gray-100 py-8" }, h("h1", { class: "text-4xl font-extrabold text-center mb-8" }, pageTitle), h(Level2, { sectionTitle: "Primary Section", isVisible: true }), h(Level2, { sectionTitle: "Secondary Section", isVisible: true }) ); const ShallowTree = () => h(Level1, { pageTitle: "Shallow Tree Benchmark" }); // DeepTree const Wrapper = ({ depth, maxDepth, children }) => { const percentage = (depth / maxDepth) * 100; return h( "div", { class: `depth-${depth}`, "data-testid": `level-${depth}`, style: { paddingLeft: "2px", borderLeft: "1px solid rgba(0,0,0,0.1)" }, }, h("span", { class: "text-xs text-gray-400" }, `Level ${depth} (${percentage.toFixed(0)}%)`), children ); }; const renderDepth = (current, max) => { if (current >= max) { return h( "div", { class: "leaf-node bg-green-100 p-2 rounded" }, h("strong", null, "Leaf Node"), h("p", { class: "text-sm" }, `Reached depth ${current}`) ); } return h(Wrapper, { depth: current, maxDepth: max }, renderDepth(current + 1, max)); }; const DeepTree50 = () => renderDepth(0, 50); // WideTree const Card = ({ id, title, description, price, rating, inStock }) => h( "article", { class: `border rounded-lg p-4 shadow-sm ${!inStock ? "opacity-50" : ""}` }, h( "div", { class: "flex justify-between items-start mb-2" }, h("h3", { class: "font-semibold text-lg" }, title), h("span", { class: "text-xs bg-gray-100 px-2 py-1 rounded" }, `#${id}`) ), h("p", { class: "text-gray-600 text-sm mb-3" }, description), h( "div", { class: "flex justify-between items-center" }, h("span", { class: "text-xl font-bold text-green-600" }, `$${price.toFixed(2)}`), h( "div", { class: "flex items-center gap-1" }, h("span", { class: "text-yellow-500" }, "★"), h("span", { class: "text-sm" }, rating.toFixed(1)) ) ), h( "div", { class: "mt-2" }, inStock ? h("span", { class: "text-green-500 text-sm" }, "In Stock") : h("span", { class: "text-red-500 text-sm" }, "Out of Stock") ) ); const WideTree100 = () => { const items = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, title: `Product ${i + 1}`, description: `Description for product ${i + 1}.`, price: 9.99 + (i % 100), rating: 3.0 + (i % 20) / 10.0, inStock: i % 7 !== 0, })); return h( "div", { class: "grid grid-cols-4 gap-4 p-4" }, items.map((item) => h(Card, { key: item.id, ...item })) ); }; // Table const Table100 = () => { const users = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: `User ${i + 1}`, email: `user${i + 1}@example.com`, role: ["Engineer", "Designer", "Manager"][i % 3], })); return h( "table", { class: "min-w-full divide-y divide-gray-200" }, h( "thead", { class: "bg-gray-50" }, h( "tr", null, ["ID", "Name", "Email", "Role"].map((header) => h( "th", { key: header, class: "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase" }, header ) ) ) ), h( "tbody", { class: "bg-white divide-y divide-gray-200" }, users.map((user, i) => h( "tr", { key: user.id, class: i % 2 === 0 ? "bg-white" : "bg-gray-50" }, h("td", { class: "px-6 py-4 text-sm" }, user.id), h("td", { class: "px-6 py-4 text-sm" }, user.name), h("td", { class: "px-6 py-4 text-sm" }, user.email), h("td", { class: "px-6 py-4 text-sm" }, user.role) ) ) ) ); }; const preactScenarios = { trivial: { component: Trivial, name: "Trivial" }, shallow: { component: ShallowTree, name: "Shallow Tree" }, deep50: { component: DeepTree50, name: "Deep Tree 50" }, wide100: { component: WideTree100, name: "Wide Tree 100" }, table100: { component: Table100, name: "Table 100" }, }; // Routes app.get("/", (req, res) => { const scenarioName = req.query.scenario || "table100"; const scenario = preactScenarios[scenarioName]; if (!scenario) { return res.status(404).send(`Unknown scenario: ${scenarioName}`); } const html = renderToString(h(scenario.component)); res.setHeader("Content-Type", "text/html"); res.send(`Benchmark
${html}
`); }); app.get("/health", (req, res) => { res.json({ status: "ok", framework: "preact", pid: process.pid }); }); app.get("/scenarios", (req, res) => { const list = Object.entries(preactScenarios).map(([key, val]) => ({ key, name: val.name, })); res.json(list); }); app.listen(PORT, () => { console.log(`[preact] Server listening on http://localhost:${PORT}`); console.log(`[preact] PID: ${process.pid}`); }); ================================================ FILE: benchmark/frameworks/render-bench.ts ================================================ /** * Pure Render Benchmark - No HTTP * Comparable to streaming_bench.ml */ if (process.env.NODE_ENV !== "production") { console.error( "[render-bench] ERROR: NODE_ENV is not 'production' (got " + JSON.stringify(process.env.NODE_ENV) + ").\n" + "React's development build is slower than production. Re-run with:\n" + " NODE_ENV=production bun render-bench.ts\n" + " NODE_ENV=production node render-bench-node.mjs\n" ); process.exit(1); } import React from "react"; import ReactDOMServer from "react-dom/server"; import * as scenarios from "./shared/scenarios.jsx"; const ITERATIONS = 100; function measureTimeUs(fn: () => string): [string, number] { const start = performance.now(); const result = fn(); const end = performance.now(); return [result, (end - start) * 1000]; // Convert ms to µs } function formatTimeUs(us: number): string { if (us < 1000) return `${us.toFixed(2)}µs`; if (us < 1_000_000) return `${(us / 1000).toFixed(2)}ms`; return `${(us / 1_000_000).toFixed(2)}s`; } interface Result { name: string; avgTimeUs: number; outputBytes: number; throughputMbS: number; } function benchmark(name: string, component: React.ComponentType): Result { let totalTime = 0; let outputBytes = 0; for (let i = 0; i < ITERATIONS; i++) { const [html, timeUs] = measureTimeUs(() => ReactDOMServer.renderToString(React.createElement(component)) ); totalTime += timeUs; outputBytes = html.length; } const avgTime = totalTime / ITERATIONS; const throughput = (outputBytes / 1_000_000) / (avgTime / 1_000_000); return { name, avgTimeUs: avgTime, outputBytes, throughputMbS: throughput, }; } const runtime = typeof (globalThis as any).Bun !== "undefined" ? `Bun ${(globalThis as any).Bun.version}` : `Node ${process.versions.node}`; console.log(`Pure Render Benchmark (${runtime} + React, NODE_ENV=production)`); console.log(`Iterations per scenario: ${ITERATIONS}\n`); // Keep this list in sync with benchmark/streaming/streaming_bench.ml so each const testScenarios = [ "trivial", "shallow", "deep10", "deep50", "wide10", "wide100", "wide500", "table10", "table100", "table500", "propsSmall", "propsMedium", "ecommerce24", "ecommerce48", "dashboard", "blog50", "form", ]; const results: Result[] = []; for (const key of testScenarios) { const scenario = scenarios.scenarios[key as keyof typeof scenarios.scenarios]; if (scenario) { const result = benchmark(key, scenario.component); results.push(result); console.log(`${key}: ${formatTimeUs(result.avgTimeUs)} (${result.outputBytes}B)`); } } console.log("\n" + "=".repeat(70)); console.log("COMPARISON TABLE"); console.log("=".repeat(70)); console.log(`${"Scenario".padEnd(20)} ${"Time".padStart(12)} ${"Size".padStart(10)} ${"Throughput".padStart(12)}`); console.log("-".repeat(70)); for (const r of results) { console.log( `${r.name.padEnd(20)} ${formatTimeUs(r.avgTimeUs).padStart(12)} ${(r.outputBytes + "B").padStart(10)} ${(r.throughputMbS.toFixed(1) + "MB/s").padStart(12)}` ); } console.log("=".repeat(70)); ================================================ FILE: benchmark/frameworks/shared/Blog.jsx ================================================ // Port of benchmark/scenarios/Blog.re // Purpose: content-heavy page rendering with nested comments import React from "react"; import { cx } from "./cx.js"; const authors = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]; const avatars = ["A", "B", "C", "D", "E", "F"]; const generateComments = (count, depth) => Array.from({ length: count }, (_, i) => ({ id: i + 1, author: authors[i % authors.length], avatar: avatars[i % avatars.length], content: `This is comment #${i + 1}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`, date: `${1 + (i % 24)} hours ago`, likes: (i * 3) % 50, replies: depth > 0 ? Array.from({ length: i % 3 }, (_, j) => ({ id: i * 100 + j, author: authors[(i + j + 1) % authors.length], avatar: avatars[(i + j + 1) % avatars.length], content: `Reply to comment #${i + 1}. Great point!`, date: `${5 + j * 10} minutes ago`, likes: j * 2, replies: [], })) : [], })); const CommentComponent = ({ comment, depth }) => (
0 ? "ml-12 border-l-2 border-gray-100 pl-4" : "border-b border-gray-100", ])} >
{comment.avatar}
{comment.author} {comment.date}

{comment.content}

{comment.replies.length > 0 && (
{comment.replies.map((reply) => ( ))}
)}
); const ArticleContent = () => (

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.

Introduction

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.

"Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt."

— Famous Author

Key Concepts

Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.

  • Ut enim ad minima veniam, quis nostrum exercitationem
  • Corporis suscipit laboriosam, nisi ut aliquid ex ea commodi
  • Quis autem vel eum iure reprehenderit qui in ea voluptate
  • At vero eos et accusamus et iusto odio dignissimos ducimus

💡 Pro Tip

Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae.

Code Example

      {`let example = () => {
  let value = computeValue();
  let result = transform(value);
  process(result);
};`}
    

Conclusion

Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.

); const Sidebar = () => { const relatedPosts = [ "Understanding Server-Side Rendering", "The Future of Web Development", "Performance Optimization Tips", "Building Scalable Applications", "Modern JavaScript Frameworks", ]; const tags = ["React", "SSR", "Performance", "JavaScript", "OCaml", "Web Development", "Tutorial"]; return ( ); }; const CommentsSection = ({ comments }) => (

{`Comments (${comments.length})`}

Leave a comment