├── .envrc ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .ocamlformat ├── .rgignore ├── Makefile ├── README.md ├── dune ├── dune-project ├── dune-workspace ├── example ├── rsc-ssr │ ├── .ocamlformat │ ├── browser │ │ ├── dune │ │ ├── example.mlx │ │ └── static │ │ │ └── tachyons.css │ ├── data │ │ ├── data.ml │ │ └── dune │ ├── data_browser │ │ ├── data.ml │ │ └── dune │ ├── dune-project │ ├── react-example-rsc-ssr.opam │ ├── routing │ │ ├── dune │ │ └── routing.ml │ └── server │ │ ├── dune │ │ └── main.mlx ├── rsc │ ├── browser │ │ ├── browser.mlx │ │ ├── dune │ │ ├── static │ │ │ └── tachyons.css │ │ └── ui.mlx │ ├── dune-project │ ├── react-example-rsc.opam │ ├── routing │ │ ├── dune │ │ └── routing.ml │ └── server │ │ ├── dune │ │ └── main.mlx └── server-only │ ├── .ocamlformat │ ├── dune │ ├── dune-project │ ├── main.mlx │ └── react_example_server_only.opam ├── htmlgen.opam ├── htmlgen ├── dune ├── htmlgen.ml └── htmlgen.mli ├── package.json ├── pnpm-lock.yaml ├── react ├── .ocamlformat ├── api │ ├── dune │ └── react_api.ml ├── browser │ ├── dune │ ├── react_browser.ml │ ├── react_browser_component_map.ml │ └── react_browser_runtime.js ├── dream │ ├── .ocamlformat │ ├── dune │ ├── react_dream.ml │ └── react_dream.mli ├── ppx │ ├── dune │ ├── html.ml │ ├── ppx.ml │ ├── ppx_test.ml │ ├── ppx_test_runner │ ├── test_async_component.t │ ├── test_browser_only.t │ ├── test_component.t │ └── test_jsx.t └── server │ ├── dune │ ├── import.ml │ ├── react.ml │ ├── react.mli │ ├── reactDOM.ml │ ├── react_model.ml │ ├── react_server.ml │ ├── react_server.mli │ ├── react_server_html_props.ml │ ├── render_to_html.ml │ ├── render_to_model.ml │ └── render_to_model.mli ├── react_browser.opam ├── react_dream.opam ├── react_server.opam ├── realm.opam ├── realm ├── .ocamlformat ├── browser │ ├── dune │ └── realm.ml └── native │ ├── dune │ └── realm.ml ├── remote.opam └── remote ├── .ocamlformat ├── browser ├── dune ├── remote.ml └── remote.mli └── native ├── dune ├── remote.ml └── remote.mli /.envrc: -------------------------------------------------------------------------------- 1 | export OPAMSWITCH="$PWD" 2 | eval $(opam env) 3 | PATH_add "$PWD/_bin" 4 | PATH_add "$PWD/node_modules/.bin" 5 | export OCAMLRUNPARAM=b 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.mlx linguist-language=ocaml 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: 0 1 * * MON 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | ocaml-compiler: 19 | - "4.14" 20 | - "5.1" 21 | - "5.2" 22 | 23 | runs-on: ${{ matrix.os }} 24 | 25 | steps: 26 | - name: checkout tree 27 | uses: actions/checkout@v4 28 | 29 | - name: set-up OCaml ${{ matrix.ocaml-compiler }} 30 | uses: ocaml/setup-ocaml@v2 31 | with: 32 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 33 | opam-repositories: | 34 | default: https://github.com/ocaml/opam-repository.git 35 | andreypopp: https://github.com/andreypopp/opam-repository.git 36 | 37 | - name: set-up Node.js 21 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 21 41 | 42 | - run: opam install . --deps-only --with-test 43 | 44 | - run: opam exec -- dune build -p realm,htmlgen,remote,react_browser,react_server,react_dream 45 | 46 | - run: opam exec -- dune runtest realm remote react 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _opam 2 | _build 3 | node_modules 4 | *.install 5 | *.db 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopp/reactor/bad950f48bee80f93fdc7a77c57eb624d762ab53/.gitmodules -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | break-infix=fit-or-vertical 2 | margin=74 3 | parens-tuple=multi-line-only 4 | -------------------------------------------------------------------------------- /.rgignore: -------------------------------------------------------------------------------- 1 | deps/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPAM_PACKAGES=$(wildcard ./*.opam) $(wildcard ./*/*.opam) $(wildcard example/*/*.opam) 2 | 3 | init: opam-switch opam-install pnpm-install 4 | 5 | opam-switch: 6 | opam switch create . "5.1.1" --no-install -y 7 | opam repo add andreypopp https://github.com/andreypopp/opam-repository.git 8 | opam pin add jsonrpc.dev --dev --no-install 9 | opam pin add lsp.dev --dev --no-install 10 | opam pin add ocaml-lsp-server.dev --dev --no-install 11 | 12 | opam-install: 13 | opam install $(OPAM_PACKAGES) --deps-only -y 14 | 15 | pnpm-install: 16 | pnpm install 17 | 18 | build: 19 | dune build 20 | 21 | watch: 22 | dune build --watch 23 | 24 | fmt: 25 | dune fmt 26 | 27 | test: 28 | dune runtest 29 | 30 | react-example-rsc: 31 | dune exec -- react-example-rsc 32 | 33 | react-example-rsc-ssr: 34 | dune exec -- react-example-rsc-ssr 35 | 36 | react-example-server-only: 37 | dune exec -- react-example-server-only 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactor - React OCaml Runtime 2 | 3 | **WARNING: EXPERIMENTAL, DO NOT USE** 4 | 5 | This repository hosts the following packages: 6 | 7 | - `react` - a native OCaml implementation of the server side component for the React.js library. 8 | 9 | More specifically it implements Server Side Rendering (SSR) and React Server 10 | Components (RSC) which is interoperable with React.js, supporting Suspense. 11 | 12 | - `react_dream` - a thin layer of code to integrate `react` with `dream`. 13 | 14 | - `remote` - a data fetching library with request caching and deduplication, 15 | which works with SSR (fetched payload is being to transfered to client so no 16 | it doesn't have to be refetched). 17 | 18 | - `realm` - a compatibility layer for code written to be compiled both native 19 | OCaml toolchain and Melange. 20 | 21 | ## Run example apps 22 | 23 | The `example/` directory contains a few example apps: 24 | 25 | - `make build react-example-server-only` showcases React Server Components 26 | rendered on server to HTML and no hydration on client is performed (means no 27 | JS is needed). One can consier React being a fancy template engine in this 28 | case. 29 | 30 | - `make build react-example-rsc` showcases React Server Components which render 31 | on server to JSON model and then on client the JSON model is rendered to 32 | HTML, invoking the client components. 33 | 34 | - `make build react-example-rsc-ssr` showcases React Server Components which 35 | render on server to HTML, emitting JSON model along. On client the JSON model 36 | is being used to hydrate the server rendered HTML. 37 | 38 | All the examples feature typesafe routing with `ppx_deriving_router` and data 39 | fetching with `remote`. 40 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (dirs :standard \ node_modules) 2 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.16) 2 | (using dune_site 0.1) 3 | (using melange 0.1) 4 | 5 | (name reactor) 6 | 7 | 8 | (generate_opam_files true) 9 | 10 | (source 11 | (github andreypopp/reactor)) 12 | 13 | (authors "Andrey Popp") 14 | 15 | (maintainers "Andrey Popp") 16 | 17 | (license LICENSE) 18 | 19 | (package 20 | (name htmlgen) 21 | (depends 22 | (ocaml 23 | (>= 4.14)) 24 | dune 25 | containers)) 26 | 27 | (package 28 | (name realm) 29 | (depends 30 | (ocaml 31 | (>= 4.14)) 32 | melange 33 | dune 34 | lwt 35 | yojson)) 36 | 37 | (package 38 | (name remote) 39 | (depends 40 | (ocaml 41 | (>= 4.14)) 42 | melange 43 | dune 44 | melange-fetch 45 | containers 46 | ppxlib 47 | htmlgen 48 | hmap 49 | realm 50 | ppx_deriving_router 51 | yojson)) 52 | 53 | (package 54 | (name react_server) 55 | (depends 56 | (ocamlformat :with-test) 57 | (ocaml 58 | (>= 4.14)) 59 | dune 60 | remote 61 | realm 62 | htmlgen 63 | containers 64 | lwt 65 | ppxlib 66 | yojson)) 67 | 68 | (package 69 | (name react_browser) 70 | (depends 71 | (ocamlformat :with-test) 72 | (ocaml 73 | (>= 4.14)) 74 | melange 75 | dune 76 | reason-react 77 | remote 78 | realm 79 | ppxlib)) 80 | 81 | (package 82 | (name react_dream) 83 | (depends 84 | (ocaml 85 | (>= 4.14)) 86 | dune 87 | lwt 88 | dream 89 | htmlgen 90 | react_server)) 91 | -------------------------------------------------------------------------------- /dune-workspace: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopp/reactor/bad950f48bee80f93fdc7a77c57eb624d762ab53/dune-workspace -------------------------------------------------------------------------------- /example/rsc-ssr/.ocamlformat: -------------------------------------------------------------------------------- 1 | break-infix=fit-or-vertical 2 | margin=74 3 | parens-tuple=multi-line-only 4 | -------------------------------------------------------------------------------- /example/rsc-ssr/browser/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name example) 3 | (wrapped false) 4 | (libraries 5 | melange.dom 6 | react_browser 7 | remote.browser 8 | data_browser 9 | routing_browser 10 | realm.browser 11 | melange-webapi) 12 | (flags :standard -open Realm -open React_browser -alert ++browser_only) 13 | (modes melange) 14 | (preprocess 15 | (pps melange.ppx react_server.ppx ppx_deriving_json.browser))) 16 | 17 | (subdir 18 | native 19 | (library 20 | (name example_native) 21 | (wrapped false) 22 | (modes native) 23 | (libraries 24 | react_server.browser 25 | remote.native 26 | realm.native 27 | data_native 28 | routing_native) 29 | (flags :standard -open Realm -alert ++browser_only -w -32-27-26) 30 | (preprocess 31 | (pps react_server.ppx -native ppx_deriving_json.native))) 32 | (copy_files# 33 | (files ../**[!.pp][!.mlx].ml)) 34 | (copy_files# 35 | (files ../**.mlx))) 36 | 37 | (melange.emit 38 | (alias browser_js) 39 | (target output) 40 | (modules) 41 | (libraries example) 42 | (module_systems commonjs)) 43 | 44 | (rule 45 | (target ./__boot.js) 46 | (deps 47 | (:entry ./output/example/rsc-ssr/browser/example.js) 48 | (:runtime %{lib:react_browser:browser/react_browser_runtime.js})) 49 | (action 50 | (with-stdout-to 51 | %{target} 52 | (progn 53 | (run echo "import './%{entry}';") 54 | (run echo "import './%{runtime}';"))))) 55 | 56 | (rule 57 | (target ./bundle.js) 58 | (deps 59 | %{lib:react_browser:browser/react_browser_runtime.js} 60 | (alias browser_js)) 61 | (action 62 | (run 63 | esbuild 64 | --log-level=warning 65 | --bundle 66 | --loader:.js=jsx 67 | --outfile=%{target} 68 | ./__boot.js))) 69 | 70 | (install 71 | (package react-example-rsc-ssr) 72 | (section 73 | (site 74 | (react-example-rsc-ssr static))) 75 | (files 76 | bundle.js 77 | (static/tachyons.css as bundle.css))) 78 | -------------------------------------------------------------------------------- /example/rsc-ssr/browser/example.mlx: -------------------------------------------------------------------------------- 1 | open Routing 2 | 3 | let tag_app = Remote.Tag.make "app" 4 | 5 | let%component link' ~href ~label () = 6 | let%browser_only onClick ev = 7 | React.Event.Mouse.preventDefault ev; 8 | React_browser.Router.navigate href 9 | in 10 | (React.string label) 11 | 12 | let%component with_children ~children:_ () =
13 | 14 | let%component button' ~onPress:onClick ~label () = 15 | 18 | 19 | let%component hello ~name () = 20 | let q, setq = 21 | React.useState (fun () -> Data.Hello.fetch ~tags:[tag_app] (Hello { name })) 22 | in 23 | let () = 24 | React.useEffect1 25 | (fun () -> 26 | React.startTransition (fun () -> 27 | setq (fun _ -> Data.Hello.fetch ~tags:[tag_app] (Hello { name }))); 28 | None) 29 | [| name |] 30 | in 31 | let msg = React.use q in 32 | let%browser_only (onClick [@ocaml.warning "-26"]) = 33 | fun _ev -> 34 | ignore 35 | (Promise.( 36 | let* () = 37 | Data.Hello.run 38 | (Update_greeting { greeting = Greeting_informal }) 39 | in 40 | Data.Hello.invalidate_tags [tag_app]; 41 | React.startTransition (fun () -> 42 | setq (fun _ -> Data.Hello.fetch (Hello { name }))); 43 | return ()) 44 | : unit Promise.t) 45 | in 46 |
(React.string msg)
47 | 48 | let%component counter ~init ~title () = 49 | let v, setv = React.useState (Fun.const init) in 50 | let succ _ev = React.startTransition @@ fun () -> setv Int.succ in 51 | let pred _ev = React.startTransition @@ fun () -> setv Int.pred in 52 | let reset _ev = React.startTransition @@ fun () -> setv (Fun.const 0) in 53 |
54 |

(React.string title)

55 |

(React.string ("clicked " ^ string_of_int v ^ " times"))

56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | module Wait_and_print = struct 64 | let%component make ~promise ?promise2 ~msg () = 65 | let () = React.use promise in 66 | let () = Option.map React.use promise2 |> Option.value ~default:() in 67 |
  • (React.string msg)
  • 68 | end 69 | 70 | let%component wait_and_print ~promise ?promise2 ~msg () = 71 | let () = React.use promise in 72 | let () = Option.map React.use promise2 |> Option.value ~default:() in 73 |
  • (React.string msg)
  • 74 | 75 | let%component nav' () = 76 | 81 | 82 | type about_mode = About_light | About_dark [@@deriving json] 83 | 84 | let%export_component about ~(mode : about_mode) ~(num : int) () = 85 | let _ = num in 86 | let%browser_only () = 87 | match mode with 88 | | About_dark -> Js.log "dark" 89 | | About_light -> Js.log "light" 90 | in 91 |
    92 | 93 | let%export_component app ~(title : string) ~(inner : React.element) () = 94 | let promise = Promise.sleep 1.0 in 95 | let promise2 = Promise.sleep 2.0 in 96 | let promise_inner = Promise.sleep 0.5 in 97 | let%browser_only () = 98 | React.useEffect1 99 | (fun () -> 100 | Js.log "HELLO, I'M READY"; 101 | None) 102 | [||] 103 | in 104 |
    105 | 106 | 107 | 108 | 109 |

    "Hello, " (React.string title) "!"

    110 | 111 |
    inner inner
    112 |
      113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
    124 |
    125 | 126 | let%component todo_item ~on_completed ~mode ~(todo : Api.Todo.todo) () = 127 | let%browser_only onChange _ev = 128 | on_completed todo (not todo.completed) 129 | in 130 | let label = match mode with 131 | | `remote -> React.null 132 | | `creating -> React.string " (creating...)" 133 | | `updating -> React.string " (updating...)" 134 | in 135 |
  • 136 | 143 |
  • 144 | 145 | let%component todo_section ~title ~on_completed ~todos () = 146 | let todos = 147 | List.map 148 | (fun (mode, todo) -> 149 | let key = Int.to_string todo.Api.Todo.id in 150 | ) 151 | todos 152 | in 153 |
    154 |
    (React.string title)
    155 |
      156 | (React.array (Array.of_list todos)) 157 |
    158 |
    159 | 160 | let%component add_todo_form ~on_create () = 161 | let value, set_value = React.useState (fun () -> "") in 162 | let input, set_input = ReactDOM.Ref.useCurrentDomRef () in 163 | let%browser_only onChange ev = 164 | let value = React.Event.Form.(target ev)##value in 165 | set_value (fun _ -> value) 166 | in 167 | let%browser_only create () = 168 | on_create value; 169 | set_value (fun _ -> ""); 170 | let el = Js.Nullable.toOption input.current in 171 | let el = Option.bind el Webapi.Dom.HtmlElement.ofElement in 172 | Option.iter Webapi.Dom.HtmlElement.focus el 173 | in 174 | let%browser_only onKeyDown ev = 175 | match React.Event.Keyboard.key ev with 176 | | "Enter" -> create () 177 | | _ -> () 178 | in 179 | let%browser_only onClick _ev = create () in 180 |
    181 | 182 | 183 |
    184 | 185 | 186 | (* define how we update local state optimistically *) 187 | module Todo_state = struct 188 | (* a todo is either remote todo or a todo being created or updated *) 189 | type todo = [ `remote | `creating | `updating ] * Api.Todo.todo 190 | 191 | type action = A : _ Api.Todo.t -> action 192 | 193 | let tag_todo = Remote.Tag.make "todo" 194 | let tags = [tag_todo] 195 | 196 | let use () = 197 | let tags = [tag_todo] in 198 | let fetch_todos () = 199 | Promise.( 200 | let* todos = Data.Todo.fetch ~tags List in 201 | return (List.map (fun x -> `remote, x) todos)) 202 | in 203 | let todos, set_fetching = 204 | let fetching, set_fetching = React.useState fetch_todos in 205 | React.use fetching, set_fetching 206 | in 207 | let%browser_only update todos (A action) : todo list = 208 | match action with 209 | | List -> todos 210 | | Create { text } -> 211 | let todo = { Api.Todo.id = List.length todos; text; completed = false } in 212 | (`creating, todo)::todos 213 | | Update { id; text; completed } -> 214 | List.map (fun (mode, t) -> 215 | if t.Api.Todo.id = id then 216 | let text = Option.value ~default:t.text text in 217 | let completed = Option.value ~default:t.completed completed in 218 | `updating, { t with text; completed } 219 | else mode, t) todos 220 | | Remove_completed -> 221 | List.filter (fun (_, t) -> not t.Api.Todo.completed) todos 222 | in 223 | let todos, modify_locally = 224 | React.useOptimistic todos update 225 | in 226 | let%browser_only modify (A action) = 227 | React.startTransition @@ fun () -> 228 | modify_locally (A action); 229 | let fetching = 230 | Promise.( 231 | let* _ = Data.Todo.run action in 232 | Data.Todo.invalidate_tags tags; 233 | fetch_todos ()) 234 | in 235 | set_fetching (fun _ -> fetching) 236 | in 237 | todos, modify 238 | end 239 | 240 | let%component todo_list' () = 241 | let todos, modify = Todo_state.use () in 242 | let%browser_only on_create text = 243 | modify (A (Create { text })) 244 | in 245 | let%browser_only on_completed todo completed = 246 | modify (A (Update { id = todo.Api.Todo.id; completed = Some completed; text = None })) 247 | in 248 | let%browser_only on_remove_completed _ev = 249 | modify (A Remove_completed) 250 | in 251 | let completed, to_be_done = 252 | List.partition_map 253 | (fun (_, todo as item) -> 254 | match todo.Api.Todo.completed with 255 | | true -> Left item 256 | | false -> Right item) 257 | todos 258 | in 259 |
    260 | 261 | 262 | 263 | 264 |
    265 | 266 | let%export_component todo_list () =
    267 | 268 | let%component wait_for_promise ~promise () = 269 | let value = React.use promise in 270 |
    "Promise resolved: " (React.string value)
    271 | 272 | let%export_component demo_passing_promises_as_props ~(promise : string Promise.t) () = 273 |
    274 |
    "waiting for promise to resolve:"
    275 | "..."
    )> 276 | 277 | 278 |
    279 | 280 | let%browser_only () = 281 | Js.log "this will execute only in browser on startup" 282 | -------------------------------------------------------------------------------- /example/rsc-ssr/data/data.ml: -------------------------------------------------------------------------------- 1 | open ContainersLabels 2 | open Lwt.Infix 3 | open Routing.Api 4 | 5 | module Hello = Remote.Make (struct 6 | include Hello 7 | 8 | let current_greeting = ref "Hello" 9 | let href route = Routing.href (Routing.Api_hello route) 10 | 11 | let handle : type a. a Hello.t -> a Lwt.t = 12 | fun route -> 13 | match route with 14 | | Hello { name } -> 15 | Lwt.pause () >>= fun () -> 16 | Lwt.return 17 | (Printf.sprintf "%s, %s" !current_greeting 18 | (String.capitalize_ascii name)) 19 | | Update_greeting { greeting } -> 20 | (current_greeting := 21 | match greeting with 22 | | Greeting_formal -> "Hello" 23 | | Greeting_informal -> "HIIII"); 24 | Lwt.return () 25 | end) 26 | 27 | module Todo = Remote.Make (struct 28 | include Todo 29 | 30 | let href route = Routing.href (Routing.Api_todo route) 31 | 32 | type db = { mutable seq : int; mutable todos : todo list } 33 | 34 | let db = { seq = 0; todos = [] } 35 | 36 | let slowly () = 37 | (* Simulate a slow database operation *) 38 | Lwt_unix.sleep 3. 39 | 40 | let handle : type a. a Todo.t -> a Lwt.t = 41 | fun route -> 42 | match route with 43 | | List -> Lwt.return db.todos 44 | | Create { text } -> 45 | slowly () >>= fun () -> 46 | let todo = { id = db.seq; text; completed = false } in 47 | db.seq <- db.seq + 1; 48 | db.todos <- todo :: db.todos; 49 | Lwt.return todo 50 | | Update { id; text; completed } -> 51 | slowly () >>= fun () -> 52 | let found = ref None in 53 | db.todos <- 54 | List.map db.todos ~f:(fun todo -> 55 | if Int.equal todo.id id then ( 56 | let todo = 57 | { 58 | todo with 59 | text = Option.value text ~default:todo.text; 60 | completed = 61 | Option.value completed ~default:todo.completed; 62 | } 63 | in 64 | found := Some todo; 65 | todo) 66 | else todo); 67 | Lwt.return !found 68 | | Remove_completed -> 69 | slowly () >>= fun () -> 70 | db.todos <- 71 | List.filter db.todos ~f:(fun todo -> not todo.completed); 72 | Lwt.return () 73 | end) 74 | -------------------------------------------------------------------------------- /example/rsc-ssr/data/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name data_native) 3 | (wrapped false) 4 | (libraries routing_native remote.native)) 5 | -------------------------------------------------------------------------------- /example/rsc-ssr/data_browser/data.ml: -------------------------------------------------------------------------------- 1 | open Routing.Api 2 | 3 | module Hello = Remote.Make (struct 4 | include Hello 5 | 6 | let href route = Routing.href (Routing.Api_hello route) 7 | end) 8 | 9 | module Todo = Remote.Make (struct 10 | include Todo 11 | 12 | let href route = Routing.href (Routing.Api_todo route) 13 | end) 14 | -------------------------------------------------------------------------------- /example/rsc-ssr/data_browser/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name data_browser) 3 | (wrapped false) 4 | (modes melange) 5 | (libraries routing_browser remote.browser)) 6 | -------------------------------------------------------------------------------- /example/rsc-ssr/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.16) 2 | 3 | (using melange 0.1) 4 | 5 | (using dune_site 0.1) 6 | 7 | (generate_opam_files true) 8 | 9 | (package 10 | (name react-example-rsc-ssr) 11 | (depends 12 | (ocaml 13 | (>= 5.1)) 14 | (melange 15 | (>= 2)) 16 | dune 17 | remote 18 | realm 19 | dream 20 | lwt 21 | melange-webapi 22 | dune-site 23 | mlx 24 | ocamlmerlin-mlx 25 | ocamlformat 26 | ocamlformat-mlx 27 | ocaml-lsp-server 28 | ppx_deriving_json 29 | ppx_deriving_router 30 | yojson) 31 | (sites 32 | (share static))) 33 | 34 | (dialect 35 | (name mlx) 36 | (implementation 37 | (extension mlx) 38 | (merlin_reader mlx) 39 | (format 40 | (run ocamlformat-mlx %{input-file})) 41 | (preprocess 42 | (run mlx-pp %{input-file})))) 43 | -------------------------------------------------------------------------------- /example/rsc-ssr/react-example-rsc-ssr.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | depends: [ 4 | "ocaml" {>= "5.1"} 5 | "melange" {>= "2"} 6 | "dune" {>= "3.16"} 7 | "remote" 8 | "realm" 9 | "dream" 10 | "lwt" 11 | "melange-webapi" 12 | "dune-site" 13 | "mlx" 14 | "ocamlmerlin-mlx" 15 | "ocamlformat" 16 | "ocamlformat-mlx" 17 | "ocaml-lsp-server" 18 | "ppx_deriving_json" 19 | "ppx_deriving_router" 20 | "yojson" 21 | "odoc" {with-doc} 22 | ] 23 | build: [ 24 | ["dune" "subst"] {dev} 25 | [ 26 | "dune" 27 | "build" 28 | "-p" 29 | name 30 | "-j" 31 | jobs 32 | "--promote-install-files=false" 33 | "@install" 34 | "@runtest" {with-test} 35 | "@doc" {with-doc} 36 | ] 37 | ["dune" "install" "-p" name "--create-install-files" name] 38 | ] 39 | -------------------------------------------------------------------------------- /example/rsc-ssr/routing/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name routing_native) 3 | (wrapped false) 4 | (preprocess 5 | (pps ppx_deriving_json.native ppx_deriving_router.dream)) 6 | (modes native)) 7 | 8 | (subdir 9 | browser 10 | (library 11 | (name routing_browser) 12 | (wrapped false) 13 | (modes melange) 14 | (preprocess 15 | (pps melange.ppx ppx_deriving_json.browser ppx_deriving_router.browser))) 16 | (copy_files# 17 | (files ../**[!.pp][!.mlx].ml))) 18 | -------------------------------------------------------------------------------- /example/rsc-ssr/routing/routing.ml: -------------------------------------------------------------------------------- 1 | open Ppx_deriving_json_runtime.Primitives 2 | open Ppx_deriving_router_runtime.Primitives 3 | 4 | module Api = struct 5 | module Hello = struct 6 | type greeting = Greeting_formal | Greeting_informal 7 | [@@deriving json] 8 | 9 | type _ t = 10 | | Hello : { name : string } -> string t [@GET "/"] 11 | | Update_greeting : { greeting : greeting [@body] } -> unit t 12 | [@POST "/"] 13 | [@@deriving router] 14 | end 15 | 16 | module Todo = struct 17 | type todo = { id : int; text : string; completed : bool } 18 | [@@deriving json] 19 | 20 | type _ t = 21 | | List : todo list t [@GET "/"] 22 | | Create : { text : string } -> todo t [@POST "/"] 23 | | Remove_completed : unit t [@DELETE "/completed"] 24 | | Update : { 25 | id : int; 26 | text : string option; 27 | completed : bool option; [@body] 28 | } 29 | -> todo option t [@PUT "/:id"] 30 | [@@deriving router] 31 | end 32 | end 33 | 34 | type _ t = 35 | | Home : Ppx_deriving_router_runtime.response t [@GET "/"] 36 | | About : Ppx_deriving_router_runtime.response t [@GET "/about"] 37 | | Todo : Ppx_deriving_router_runtime.response t [@GET "/todo"] 38 | | No_ssr : Ppx_deriving_router_runtime.response t [@GET "/no-ssr"] 39 | | Api_todo : 'a Api.Todo.t -> 'a t [@prefix "/api/todo"] 40 | | Api_hello : 'a Api.Hello.t -> 'a t [@prefix "/api/hello"] 41 | [@@deriving router] 42 | -------------------------------------------------------------------------------- /example/rsc-ssr/server/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (public_name react-example-rsc-ssr) 4 | (package react-example-rsc-ssr) 5 | (libraries 6 | routing_native 7 | react_server 8 | react_dream 9 | data_native 10 | dream 11 | example_native 12 | dune-site) 13 | (flags :standard -alert ++browser_only) 14 | (preprocess 15 | (pps react_server.ppx -native ppx_deriving_json.native))) 16 | 17 | (generate_sites_module 18 | (module static) 19 | (sites react-example-rsc-ssr)) 20 | -------------------------------------------------------------------------------- /example/rsc-ssr/server/main.mlx: -------------------------------------------------------------------------------- 1 | open! ContainersLabels 2 | open! Monomorphic 3 | open Lwt.Infix 4 | 5 | module UI = struct 6 | open React_server 7 | 8 | let%async_component card ~delay ~title ~children () = 9 | Lwt_unix.sleep delay >|= fun () -> 10 |
    11 |

    (React.string title)

    12 |
    children
    13 |
    14 | "I've been sleeping for " 15 | (React.stringf "%0.1f sec" delay) 16 | " before appearing" 17 |
    18 |
    19 | 20 | let%component page ~title ~children () = 21 | 22 | (React.string title) 23 | 24 |

    (React.string title)

    children
    25 | 26 | 27 | 28 | let app () = 29 | let promise_as_prop = 30 | Lwt_unix.sleep 3. >>= fun () -> Lwt.return "PROMISE VALUE HERE" 31 | in 32 | 33 |
    34 |

    "Demo passing promises as props"

    35 | 36 |
    37 |
    38 | 42 | 43 | "This components loads some async data but will block the \ 44 | shell until this data is ready." 45 | 46 | 47 | (React.string "HELLO") 48 | (React.string "HELLO") 49 | 50 | 51 | (React.string "OUTER") 52 | 53 | 54 | (React.string "INNER") 55 | 56 | 57 | 58 |
    59 |

    (React.string "Testing XSS")

    60 |
      61 |
    • ""
    • 62 |
    • "\u{2028}"
    • 63 |
    64 |
    65 |
    66 |
    67 | 68 | let about = 69 | 70 |
    71 |

    (React.string "Just an about page")

    72 | 73 |
    74 |
    75 | 76 | let todos = 77 | 78 |
    79 | 80 |
    81 |
    82 | end 83 | 84 | let links = [ "/static/bundle.css" ] 85 | let scripts = [ "/static/bundle.js" ] 86 | 87 | let render ?enable_ssr ui = 88 | React_dream.render ?enable_ssr ~enable_client_components:true ~links 89 | ~scripts ui 90 | 91 | let handle = 92 | let f : 93 | type a. 94 | a Routing.t -> 95 | Dream.request -> 96 | a Ppx_deriving_router_runtime.return Lwt.t = 97 | fun route -> 98 | match route with 99 | | Routing.Home -> render (UI.app ()) 100 | | About -> render UI.about 101 | | Todo -> render UI.todos 102 | | No_ssr -> render ~enable_ssr:false (UI.app ()) 103 | | Api_hello route -> fun _req -> Data.Hello.handle route 104 | | Api_todo route -> fun _req -> Data.Todo.handle route 105 | in 106 | Routing.handle { f } 107 | 108 | let () = 109 | let static = 110 | Static.Sites.static 111 | |> List.head_opt 112 | |> Option.get_exn_or "no /static dir found" 113 | in 114 | Dream.run ~interface:"127.0.0.1" ~port:8080 115 | @@ Dream.logger 116 | @@ Dream.router 117 | [ 118 | Dream.get "static/**" (Dream.static static); 119 | Dream.any "**" handle; 120 | ] 121 | -------------------------------------------------------------------------------- /example/rsc/browser/browser.mlx: -------------------------------------------------------------------------------- 1 | open Routing 2 | 3 | let%export_component home ~(promise : string Promise.t) () = 4 |
    5 |

    "Home"

    6 |

    "Welcome to the home page!"

    7 |

    "Loading some data from server below:"

    8 |

    9 | 11 | 12 | 13 |

    14 |

    15 | 17 | 18 | 19 |

    20 | "About" 21 | 22 |
    23 | 24 | let%export_component about () = 25 |
    26 |

    "About"

    "Home" 27 |
    28 | -------------------------------------------------------------------------------- /example/rsc/browser/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name browser) 3 | (wrapped false) 4 | (libraries 5 | melange.dom 6 | react_browser 7 | remote.browser 8 | routing_browser 9 | realm.browser 10 | melange-webapi) 11 | (flags 12 | :standard 13 | -open=Realm 14 | -open=React_browser 15 | -open=Ppx_deriving_json_runtime.Primitives 16 | -w 17 | -33 18 | -alert 19 | ++browser_only) 20 | (modes melange) 21 | (preprocess 22 | (pps melange.ppx react_server.ppx ppx_deriving_json.browser))) 23 | 24 | (melange.emit 25 | (alias browser_js) 26 | (target output) 27 | (modules) 28 | (libraries browser) 29 | (module_systems commonjs)) 30 | 31 | (rule 32 | (target ./__boot.js) 33 | (deps 34 | (:entry ./output/example/rsc/browser/browser.js) 35 | (:runtime %{lib:react_browser:browser/react_browser_runtime.js})) 36 | (action 37 | (with-stdout-to 38 | %{target} 39 | (progn 40 | (run echo "import './%{entry}';") 41 | (run echo "import './%{runtime}';"))))) 42 | 43 | (rule 44 | (target ./bundle.js) 45 | (deps 46 | %{lib:react_browser:browser/react_browser_runtime.js} 47 | (alias browser_js)) 48 | (action 49 | (run 50 | esbuild 51 | --log-level=warning 52 | --bundle 53 | --loader:.js=jsx 54 | --outfile=%{target} 55 | ./__boot.js))) 56 | 57 | (install 58 | (package react-example-rsc) 59 | (section 60 | (site 61 | (react-example-rsc static))) 62 | (files 63 | bundle.js 64 | (static/tachyons.css as bundle.css))) 65 | -------------------------------------------------------------------------------- /example/rsc/browser/ui.mlx: -------------------------------------------------------------------------------- 1 | let%component some_ui () = 2 | Js.log "rendering ok"; 3 |
    "ok"
    4 | 5 | let%component use_promise ~promise () = 6 | let v = React.use promise in 7 | React.string ("a message from server: " ^ v) 8 | 9 | let use_interval ~ms f = 10 | React.useEffect1 11 | (fun () -> 12 | let t = Js.Global.setInterval ms ~f in 13 | Some (fun () -> Js.Global.clearInterval t)) 14 | [| ms |] 15 | 16 | (* instantiate client from the routes defintion *) 17 | module Fetch = Ppx_deriving_router_runtime.Make_fetch (Routing) 18 | 19 | let%component load_server_time () = 20 | (* fetch the server time *) 21 | let start_fetch _ = Fetch.fetch Routing.Api_server_time in 22 | (* initial fetch *) 23 | let fetching, set_fetching = React.useState start_fetch in 24 | (* re-fetch every second *) 25 | use_interval ~ms:1000 (fun () -> 26 | (* here we use React.startTransition so that suspense fallback won't be 27 | shown during refetches, instead prev data is shown *) 28 | React.startTransition @@ fun () -> set_fetching start_fetch); 29 | let t = React.use fetching in 30 | let t = Js.Date.fromFloat (t *. 1000.) in 31 | React.string ("server time: " ^ Js.Date.toLocaleString t) 32 | -------------------------------------------------------------------------------- /example/rsc/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.16) 2 | 3 | (using melange 0.1) 4 | 5 | (using dune_site 0.1) 6 | 7 | (generate_opam_files true) 8 | 9 | (package 10 | (name react-example-rsc) 11 | (depends 12 | (ocaml 13 | (>= 5.1)) 14 | (melange 15 | (>= 2)) 16 | dune 17 | remote 18 | realm 19 | dream 20 | lwt 21 | melange-webapi 22 | dune-site 23 | mlx 24 | ocamlmerlin-mlx 25 | ocamlformat 26 | ocamlformat-mlx 27 | ocaml-lsp-server 28 | ppx_deriving_json 29 | ppx_deriving_router 30 | yojson) 31 | (sites 32 | (share static))) 33 | 34 | (dialect 35 | (name mlx) 36 | (implementation 37 | (extension mlx) 38 | (merlin_reader mlx) 39 | (format 40 | (run ocamlformat-mlx %{input-file})) 41 | (preprocess 42 | (run mlx-pp %{input-file})))) 43 | -------------------------------------------------------------------------------- /example/rsc/react-example-rsc.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | depends: [ 4 | "ocaml" {>= "5.1"} 5 | "melange" {>= "2"} 6 | "dune" {>= "3.16"} 7 | "remote" 8 | "realm" 9 | "dream" 10 | "lwt" 11 | "melange-webapi" 12 | "dune-site" 13 | "mlx" 14 | "ocamlmerlin-mlx" 15 | "ocamlformat" 16 | "ocamlformat-mlx" 17 | "ocaml-lsp-server" 18 | "ppx_deriving_json" 19 | "ppx_deriving_router" 20 | "yojson" 21 | "odoc" {with-doc} 22 | ] 23 | build: [ 24 | ["dune" "subst"] {dev} 25 | [ 26 | "dune" 27 | "build" 28 | "-p" 29 | name 30 | "-j" 31 | jobs 32 | "--promote-install-files=false" 33 | "@install" 34 | "@runtest" {with-test} 35 | "@doc" {with-doc} 36 | ] 37 | ["dune" "install" "-p" name "--create-install-files" name] 38 | ] 39 | -------------------------------------------------------------------------------- /example/rsc/routing/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name routing_server) 3 | (wrapped false) 4 | (preprocess 5 | (pps ppx_deriving_json.native ppx_deriving_router.dream)) 6 | (modes native)) 7 | 8 | (subdir 9 | browser 10 | (library 11 | (name routing_browser) 12 | (wrapped false) 13 | (modes melange) 14 | (preprocess 15 | (pps melange.ppx ppx_deriving_json.browser ppx_deriving_router.browser))) 16 | (copy_files# 17 | (only_sources) 18 | (files ../*.{ml,mlx}))) 19 | -------------------------------------------------------------------------------- /example/rsc/routing/routing.ml: -------------------------------------------------------------------------------- 1 | open Ppx_deriving_json_runtime.Primitives 2 | 3 | type _ t = 4 | | Home : Ppx_deriving_router_runtime.response t [@GET "/"] 5 | | About : Ppx_deriving_router_runtime.response t [@GET "/about"] 6 | | Api_server_time : float t [@GET "/api/server-time"] 7 | [@@deriving router] 8 | -------------------------------------------------------------------------------- /example/rsc/server/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (public_name react-example-rsc) 4 | (package react-example-rsc) 5 | (libraries routing_server react_server react_dream dream dune-site) 6 | (flags 7 | :standard 8 | -open=Realm 9 | -open=React_server 10 | -open=Ppx_deriving_json_runtime.Primitives 11 | -w 12 | -33 13 | -alert 14 | ++browser_only) 15 | (preprocess 16 | (pps react_server.ppx -native-export-only ppx_deriving_json.native))) 17 | 18 | (copy_files# 19 | (only_sources) 20 | (files ../browser/browser.mlx)) 21 | 22 | (generate_sites_module 23 | (module static) 24 | (sites react-example-rsc)) 25 | -------------------------------------------------------------------------------- /example/rsc/server/main.mlx: -------------------------------------------------------------------------------- 1 | open! ContainersLabels 2 | open! Monomorphic 3 | 4 | module UI = struct 5 | open React_server 6 | 7 | let%component page ~title ~children () = 8 | 9 | (React.string title) 10 | 11 |

    (React.string title)

    children
    12 | 13 | 14 | 15 | let home () = 16 | let open Lwt.Infix in 17 | let promise = 18 | Lwt_unix.sleep 1.0 >|= fun () -> "Hello from OCaml React!" 19 | in 20 | 21 | 22 | let about = 23 | end 24 | 25 | let links = [ "/static/bundle.css" ] 26 | let scripts = [ "/static/bundle.js" ] 27 | 28 | let render ui = 29 | React_dream.render ~enable_ssr:false ~enable_client_components:true 30 | ~links ~scripts ui 31 | 32 | let handle = 33 | let f : type a. a Routing.t -> Dream.request -> a Lwt.t = 34 | fun route -> 35 | match route with 36 | | Home -> render (UI.home ()) 37 | | About -> render UI.about 38 | | Api_server_time -> 39 | fun _req -> 40 | let time = Unix.time () in 41 | Lwt.return time 42 | in 43 | Routing.handle { f } 44 | 45 | let () = 46 | let static = 47 | Static.Sites.static 48 | |> List.head_opt 49 | |> Option.get_exn_or "no /static dir found" 50 | in 51 | Dream.run ~interface:"127.0.0.1" ~port:8080 52 | @@ Dream.logger 53 | @@ Dream.router 54 | [ 55 | Dream.get "static/**" (Dream.static static); 56 | Dream.any "**" handle; 57 | ] 58 | -------------------------------------------------------------------------------- /example/server-only/.ocamlformat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopp/reactor/bad950f48bee80f93fdc7a77c57eb624d762ab53/example/server-only/.ocamlformat -------------------------------------------------------------------------------- /example/server-only/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (public_name react-example-server-only) 4 | (package react_example_server_only) 5 | (libraries react_server react_dream dream) 6 | (flags :standard -alert ++browser_only) 7 | (preprocess 8 | (pps ppx_deriving_router.dream react_server.ppx -native))) 9 | -------------------------------------------------------------------------------- /example/server-only/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.16) 2 | 3 | (generate_opam_files true) 4 | 5 | (package 6 | (name react_example_server_only) 7 | (depends 8 | (ocaml 9 | (>= 5.1)) 10 | dune 11 | dream 12 | lwt 13 | ppx_deriving_router 14 | mlx 15 | ocamlmerlin-mlx 16 | ocamlformat 17 | ocamlformat-mlx 18 | ocaml-lsp-server)) 19 | 20 | (dialect 21 | (name mlx) 22 | (implementation 23 | (extension mlx) 24 | (merlin_reader mlx) 25 | (format 26 | (run ocamlformat-mlx %{input-file})) 27 | (preprocess 28 | (run mlx-pp %{input-file})))) 29 | -------------------------------------------------------------------------------- /example/server-only/main.mlx: -------------------------------------------------------------------------------- 1 | open! ContainersLabels 2 | open! Monomorphic 3 | open Lwt.Infix 4 | open React_server 5 | 6 | module Modifier = struct 7 | type t = Capitalize | Uppercase 8 | 9 | let of_url_query : t Ppx_deriving_router_runtime.Decode.decode_url_query = 10 | fun k xs -> 11 | match List.assoc_opt ~eq:String.equal k xs with 12 | | None -> Error "not found" 13 | | Some "capitalize" -> Ok Capitalize 14 | | Some "uppercase" -> Ok Uppercase 15 | | Some _ -> Error "invalid value" 16 | 17 | let to_url_query : t Ppx_deriving_router_runtime.Encode.encode_url_query = 18 | fun k -> function 19 | | Capitalize -> [ (k, "capitalize") ] 20 | | Uppercase -> [ (k, "uppercase") ] 21 | end 22 | 23 | module Routes = struct 24 | open Ppx_deriving_router_runtime.Primitives 25 | 26 | type t = 27 | | Home [@GET "/"] 28 | | Hello of { 29 | name : string; 30 | repeat : int option; 31 | modifier : Modifier.t option; 32 | } [@GET "/hello/:name"] 33 | [@@deriving router] 34 | end 35 | 36 | module UI = struct 37 | let%async_component card ~delay ~title ~children () = 38 | Lwt_unix.sleep delay >|= fun () -> 39 |
    40 |

    (React.string title)

    41 |
    children
    42 |
    43 | "I've been sleeping for " 44 | (React.stringf "%0.1f sec" delay) 45 | " before appearing" 46 |
    47 |
    48 | 49 | let%component page ~title ~children () = 50 | 51 | (React.string title) 52 | 53 |

    (React.string title)

    children
    54 | 55 | 56 | 57 | let app = 58 | 59 |
    60 | 61 | "This components loads some async data but will block the shell \ 62 | until this data is ready." 63 |
    64 | 71 | "Go to hello page" 72 | 73 |
    74 |
    75 | 76 | (React.string "HELLO") 77 | (React.string "HELLO") 78 | 79 | 80 | (React.string "OUTER") 81 | 82 | 83 | (React.string "INNER") 84 | 85 | 86 | 87 |
    88 |

    (React.string "Testing XSS")

    89 |
      90 |
    • ""
    • 91 |
    • "\u{2028}"
    • 92 |
    93 |
    94 |
    95 |
    96 | end 97 | 98 | let render ui = React_dream.render ui 99 | 100 | let pages = 101 | Routes.handle @@ fun route -> 102 | match route with 103 | | Home -> render UI.app 104 | | Hello { name; modifier; repeat } -> 105 | let name = 106 | match modifier with 107 | | Some Capitalize -> String.capitalize_ascii name 108 | | Some Uppercase -> String.uppercase_ascii name 109 | | None -> name 110 | in 111 | let name = 112 | match repeat with 113 | | None -> name 114 | | Some repeat -> 115 | List.init repeat ~f:(fun _ -> name) |> String.concat ~sep:", " 116 | in 117 | render 118 |
    119 |
    "Hello, " (React.string name)
    120 |
    "Back to " "main page"
    121 |
    122 | 123 | let () = Dream.run ~interface:"127.0.0.1" ~port:8080 @@ Dream.logger @@ pages 124 | -------------------------------------------------------------------------------- /example/server-only/react_example_server_only.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | depends: [ 4 | "ocaml" {>= "5.1"} 5 | "dune" {>= "3.16"} 6 | "dream" 7 | "lwt" 8 | "ppx_deriving_router" 9 | "mlx" 10 | "ocamlmerlin-mlx" 11 | "ocamlformat" 12 | "ocamlformat-mlx" 13 | "ocaml-lsp-server" 14 | "odoc" {with-doc} 15 | ] 16 | build: [ 17 | ["dune" "subst"] {dev} 18 | [ 19 | "dune" 20 | "build" 21 | "-p" 22 | name 23 | "-j" 24 | jobs 25 | "@install" 26 | "@runtest" {with-test} 27 | "@doc" {with-doc} 28 | ] 29 | ] 30 | -------------------------------------------------------------------------------- /htmlgen.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: ["Andrey Popp"] 4 | authors: ["Andrey Popp"] 5 | license: "LICENSE" 6 | homepage: "https://github.com/andreypopp/reactor" 7 | bug-reports: "https://github.com/andreypopp/reactor/issues" 8 | depends: [ 9 | "ocaml" {>= "4.14"} 10 | "dune" {>= "3.16"} 11 | "containers" 12 | "odoc" {with-doc} 13 | ] 14 | build: [ 15 | ["dune" "subst"] {dev} 16 | [ 17 | "dune" 18 | "build" 19 | "-p" 20 | name 21 | "-j" 22 | jobs 23 | "--promote-install-files=false" 24 | "@install" 25 | "@runtest" {with-test} 26 | "@doc" {with-doc} 27 | ] 28 | ["dune" "install" "-p" name "--create-install-files" name] 29 | ] 30 | dev-repo: "git+https://github.com/andreypopp/reactor.git" 31 | -------------------------------------------------------------------------------- /htmlgen/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name htmlgen) 3 | (name htmlgen) 4 | (libraries containers)) 5 | -------------------------------------------------------------------------------- /htmlgen/htmlgen.ml: -------------------------------------------------------------------------------- 1 | (* Contains code form borrowed from https://github.com/dbuenzli/htmlit/blob/main/src/htmlit.ml 2 | 3 | Copyright (c) 2016 The htmlit programmers. All rights reserved. 4 | SPDX-License-Identifier: ISC 5 | *) 6 | 7 | open Printf 8 | open ContainersLabels 9 | open Monomorphic 10 | 11 | type attrs = attr list 12 | and attr = string * attr_value 13 | 14 | and attr_value = 15 | [ `String of string | `Bool of bool | `Int of int | `Float of float ] 16 | 17 | type t = 18 | | H_node of string * attrs * t list 19 | | H_text of string 20 | | H_raw of string 21 | | H_splice of t list * string 22 | 23 | let node name props children = H_node (name, props, children) 24 | let text text = H_text text 25 | let empty = H_raw "" 26 | let unsafe_raw data = H_raw data 27 | let unsafe_rawf fmt = ksprintf unsafe_raw fmt 28 | let splice ?(sep = "") xs = H_splice (xs, sep) 29 | let s v = `String v 30 | let b v = `Bool v 31 | let i v = `Int v 32 | let f v = `Float v 33 | 34 | let add_escaped b s = 35 | let adds = Buffer.add_string in 36 | let len = String.length s in 37 | let max_idx = len - 1 in 38 | let flush b start i = 39 | if start < len then Buffer.add_substring b s start (i - start) 40 | in 41 | let rec loop start i = 42 | if i > max_idx then flush b start i 43 | else 44 | let next = i + 1 in 45 | match String.get s i with 46 | | '&' -> 47 | flush b start i; 48 | adds b "&"; 49 | loop next next 50 | | '<' -> 51 | flush b start i; 52 | adds b "<"; 53 | loop next next 54 | | '>' -> 55 | flush b start i; 56 | adds b ">"; 57 | loop next next 58 | | '\'' -> 59 | flush b start i; 60 | adds b "'"; 61 | loop next next 62 | | '\"' -> 63 | flush b start i; 64 | adds b """; 65 | loop next next 66 | | _ -> loop start next 67 | in 68 | loop 0 0 69 | 70 | let is_void_element = function 71 | | "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" 72 | | "link" | "meta" | "param" | "source" | "track" | "wbr" -> 73 | true 74 | | _ -> false 75 | 76 | let rec write buf = 77 | let adds s = Buffer.add_string buf s in 78 | function 79 | | H_splice (xs, sep) -> 80 | let rec aux = function 81 | | [] -> () 82 | | [ x ] -> write buf x 83 | | [ x; y ] -> 84 | write buf x; 85 | adds sep; 86 | write buf y 87 | | x :: xs -> 88 | write buf x; 89 | adds sep; 90 | aux xs 91 | in 92 | aux xs 93 | | H_raw data -> adds data 94 | | H_text text -> add_escaped buf text 95 | | H_node (name, props, children) -> ( 96 | adds "<"; 97 | adds name; 98 | let () = 99 | match props with 100 | | [] -> () 101 | | attrs -> 102 | List.iter attrs ~f:(fun (name, value) -> 103 | adds " "; 104 | let name = 105 | match name with "className" -> "class" | name -> name 106 | in 107 | match value with 108 | | `Bool false -> () 109 | | `Bool true -> adds name 110 | | `Int v -> 111 | adds name; 112 | adds (sprintf "=\"%i\"" v) 113 | | `Float v -> 114 | adds name; 115 | adds (sprintf "=\"%f\"" v) 116 | | `String value -> 117 | adds name; 118 | adds "=\""; 119 | add_escaped buf value; 120 | adds "\"") 121 | in 122 | match children with 123 | | [] -> 124 | if is_void_element name then adds ">" 125 | else ( 126 | adds ">") 129 | | children -> 130 | adds ">"; 131 | List.iter children ~f:(write buf); 132 | adds "") 135 | 136 | let to_string html = 137 | let buf = Buffer.create 1024 in 138 | write buf html; 139 | Buffer.contents buf 140 | 141 | let add_single_quote_escaped b s = 142 | let getc = String.unsafe_get s in 143 | let adds = Buffer.add_string in 144 | let len = String.length s in 145 | let max_idx = len - 1 in 146 | let flush b start i = 147 | if start < len then Buffer.add_substring b s start (i - start) 148 | in 149 | let rec loop start i = 150 | if i > max_idx then flush b start i 151 | else 152 | let next = i + 1 in 153 | match getc i with 154 | | '\'' -> 155 | flush b start i; 156 | adds b "'"; 157 | loop next next 158 | | _ -> loop start next 159 | in 160 | loop 0 0 161 | 162 | let single_quote_escape data = 163 | let buf = Buffer.create (String.length data) in 164 | add_single_quote_escaped buf data; 165 | Buffer.contents buf 166 | -------------------------------------------------------------------------------- /htmlgen/htmlgen.mli: -------------------------------------------------------------------------------- 1 | (** Combinators for HTML generation. 2 | 3 | Contains code from https://github.com/dbuenzli/htmlit/blob/main/src/htmlit.ml 4 | 5 | Copyright (c) 2016 The htmlit programmers. All rights reserved. 6 | SPDX-License-Identifier: ISC 7 | *) 8 | 9 | (** {1 HTML model} *) 10 | 11 | (** {2 HTML attributes} *) 12 | 13 | type attrs = (string * attr_value) list 14 | (** A list of HTML attributes. *) 15 | 16 | and attr = string * attr_value 17 | 18 | and attr_value = 19 | [ `String of string | `Bool of bool | `Int of int | `Float of float ] 20 | 21 | val s : string -> attr_value 22 | val b : bool -> attr_value 23 | val i : int -> attr_value 24 | val f : float -> attr_value 25 | 26 | (** {2 HTML elements} *) 27 | 28 | type t 29 | (** An HTML element. *) 30 | 31 | val node : string -> attrs -> t list -> t 32 | (** [node tag attrs children] produces an HTML tag with specified attributes and 33 | children elements. 34 | 35 | e.g 36 | {v node "div" ["className", s "block"] [ text "hello" ] v} 37 | renders into 38 | {v
    hello
    v} 39 | 40 | *) 41 | 42 | val text : string -> t 43 | (** [text s] renders [s] string, escaping its content. *) 44 | 45 | val splice : ?sep:string -> t list -> t 46 | (** [splice ~sep xs] concats [xs] elements together using [sep] separator (by 47 | default it is empty) *) 48 | 49 | val empty : t 50 | (** [empty] renders nothing *) 51 | 52 | val unsafe_raw : string -> t 53 | (** Unsafely (without any escaping) embed a string into HTML. Never use this 54 | untrusted input. *) 55 | 56 | val unsafe_rawf : ('a, unit, string, t) format4 -> 'a 57 | (** Same as [unsafe_raw] but allows to use a format string. Never use this 58 | untrusted input. *) 59 | 60 | (** {1 HTML Rendering} *) 61 | 62 | val to_string : t -> string 63 | (** Render HTML to string. *) 64 | 65 | (** {1 Various utilities} *) 66 | 67 | val single_quote_escape : string -> string 68 | (** Escape ['] single quote as HTML entity. This can be used to embed arbitray 69 | values within the ['] single quoted HTML attribute. Use with care. *) 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactor", 3 | "version": "1.0.0", 4 | "author": "Andrey Popp", 5 | "license": "ISC", 6 | "dependencies": { 7 | "esbuild": "^0.20.2", 8 | "react": "rc", 9 | "react-dom": "rc", 10 | "react-server-dom-webpack": "rc", 11 | "reactor": "link:" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | esbuild: 9 | specifier: ^0.20.2 10 | version: 0.20.2 11 | react: 12 | specifier: rc 13 | version: 19.0.0-rc-cc1ec60d0d-20240607 14 | react-dom: 15 | specifier: rc 16 | version: 19.0.0-rc-cc1ec60d0d-20240607(react@19.0.0-rc-cc1ec60d0d-20240607) 17 | react-server-dom-webpack: 18 | specifier: rc 19 | version: 19.0.0-rc-cc1ec60d0d-20240607(react-dom@19.0.0-rc-cc1ec60d0d-20240607)(react@19.0.0-rc-cc1ec60d0d-20240607)(webpack@5.88.2) 20 | reactor: 21 | specifier: 'link:' 22 | version: 'link:' 23 | 24 | packages: 25 | 26 | /@esbuild/aix-ppc64@0.20.2: 27 | resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} 28 | engines: {node: '>=12'} 29 | cpu: [ppc64] 30 | os: [aix] 31 | requiresBuild: true 32 | dev: false 33 | optional: true 34 | 35 | /@esbuild/android-arm64@0.20.2: 36 | resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} 37 | engines: {node: '>=12'} 38 | cpu: [arm64] 39 | os: [android] 40 | requiresBuild: true 41 | dev: false 42 | optional: true 43 | 44 | /@esbuild/android-arm@0.20.2: 45 | resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} 46 | engines: {node: '>=12'} 47 | cpu: [arm] 48 | os: [android] 49 | requiresBuild: true 50 | dev: false 51 | optional: true 52 | 53 | /@esbuild/android-x64@0.20.2: 54 | resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} 55 | engines: {node: '>=12'} 56 | cpu: [x64] 57 | os: [android] 58 | requiresBuild: true 59 | dev: false 60 | optional: true 61 | 62 | /@esbuild/darwin-arm64@0.20.2: 63 | resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} 64 | engines: {node: '>=12'} 65 | cpu: [arm64] 66 | os: [darwin] 67 | requiresBuild: true 68 | dev: false 69 | optional: true 70 | 71 | /@esbuild/darwin-x64@0.20.2: 72 | resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} 73 | engines: {node: '>=12'} 74 | cpu: [x64] 75 | os: [darwin] 76 | requiresBuild: true 77 | dev: false 78 | optional: true 79 | 80 | /@esbuild/freebsd-arm64@0.20.2: 81 | resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} 82 | engines: {node: '>=12'} 83 | cpu: [arm64] 84 | os: [freebsd] 85 | requiresBuild: true 86 | dev: false 87 | optional: true 88 | 89 | /@esbuild/freebsd-x64@0.20.2: 90 | resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} 91 | engines: {node: '>=12'} 92 | cpu: [x64] 93 | os: [freebsd] 94 | requiresBuild: true 95 | dev: false 96 | optional: true 97 | 98 | /@esbuild/linux-arm64@0.20.2: 99 | resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} 100 | engines: {node: '>=12'} 101 | cpu: [arm64] 102 | os: [linux] 103 | requiresBuild: true 104 | dev: false 105 | optional: true 106 | 107 | /@esbuild/linux-arm@0.20.2: 108 | resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} 109 | engines: {node: '>=12'} 110 | cpu: [arm] 111 | os: [linux] 112 | requiresBuild: true 113 | dev: false 114 | optional: true 115 | 116 | /@esbuild/linux-ia32@0.20.2: 117 | resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} 118 | engines: {node: '>=12'} 119 | cpu: [ia32] 120 | os: [linux] 121 | requiresBuild: true 122 | dev: false 123 | optional: true 124 | 125 | /@esbuild/linux-loong64@0.20.2: 126 | resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} 127 | engines: {node: '>=12'} 128 | cpu: [loong64] 129 | os: [linux] 130 | requiresBuild: true 131 | dev: false 132 | optional: true 133 | 134 | /@esbuild/linux-mips64el@0.20.2: 135 | resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} 136 | engines: {node: '>=12'} 137 | cpu: [mips64el] 138 | os: [linux] 139 | requiresBuild: true 140 | dev: false 141 | optional: true 142 | 143 | /@esbuild/linux-ppc64@0.20.2: 144 | resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} 145 | engines: {node: '>=12'} 146 | cpu: [ppc64] 147 | os: [linux] 148 | requiresBuild: true 149 | dev: false 150 | optional: true 151 | 152 | /@esbuild/linux-riscv64@0.20.2: 153 | resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} 154 | engines: {node: '>=12'} 155 | cpu: [riscv64] 156 | os: [linux] 157 | requiresBuild: true 158 | dev: false 159 | optional: true 160 | 161 | /@esbuild/linux-s390x@0.20.2: 162 | resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} 163 | engines: {node: '>=12'} 164 | cpu: [s390x] 165 | os: [linux] 166 | requiresBuild: true 167 | dev: false 168 | optional: true 169 | 170 | /@esbuild/linux-x64@0.20.2: 171 | resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} 172 | engines: {node: '>=12'} 173 | cpu: [x64] 174 | os: [linux] 175 | requiresBuild: true 176 | dev: false 177 | optional: true 178 | 179 | /@esbuild/netbsd-x64@0.20.2: 180 | resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} 181 | engines: {node: '>=12'} 182 | cpu: [x64] 183 | os: [netbsd] 184 | requiresBuild: true 185 | dev: false 186 | optional: true 187 | 188 | /@esbuild/openbsd-x64@0.20.2: 189 | resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} 190 | engines: {node: '>=12'} 191 | cpu: [x64] 192 | os: [openbsd] 193 | requiresBuild: true 194 | dev: false 195 | optional: true 196 | 197 | /@esbuild/sunos-x64@0.20.2: 198 | resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} 199 | engines: {node: '>=12'} 200 | cpu: [x64] 201 | os: [sunos] 202 | requiresBuild: true 203 | dev: false 204 | optional: true 205 | 206 | /@esbuild/win32-arm64@0.20.2: 207 | resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} 208 | engines: {node: '>=12'} 209 | cpu: [arm64] 210 | os: [win32] 211 | requiresBuild: true 212 | dev: false 213 | optional: true 214 | 215 | /@esbuild/win32-ia32@0.20.2: 216 | resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} 217 | engines: {node: '>=12'} 218 | cpu: [ia32] 219 | os: [win32] 220 | requiresBuild: true 221 | dev: false 222 | optional: true 223 | 224 | /@esbuild/win32-x64@0.20.2: 225 | resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} 226 | engines: {node: '>=12'} 227 | cpu: [x64] 228 | os: [win32] 229 | requiresBuild: true 230 | dev: false 231 | optional: true 232 | 233 | /@jridgewell/gen-mapping@0.3.3: 234 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 235 | engines: {node: '>=6.0.0'} 236 | dependencies: 237 | '@jridgewell/set-array': 1.1.2 238 | '@jridgewell/sourcemap-codec': 1.4.15 239 | '@jridgewell/trace-mapping': 0.3.18 240 | dev: false 241 | 242 | /@jridgewell/resolve-uri@3.1.0: 243 | resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} 244 | engines: {node: '>=6.0.0'} 245 | dev: false 246 | 247 | /@jridgewell/set-array@1.1.2: 248 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 249 | engines: {node: '>=6.0.0'} 250 | dev: false 251 | 252 | /@jridgewell/source-map@0.3.5: 253 | resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} 254 | dependencies: 255 | '@jridgewell/gen-mapping': 0.3.3 256 | '@jridgewell/trace-mapping': 0.3.18 257 | dev: false 258 | 259 | /@jridgewell/sourcemap-codec@1.4.14: 260 | resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} 261 | dev: false 262 | 263 | /@jridgewell/sourcemap-codec@1.4.15: 264 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 265 | dev: false 266 | 267 | /@jridgewell/trace-mapping@0.3.18: 268 | resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} 269 | dependencies: 270 | '@jridgewell/resolve-uri': 3.1.0 271 | '@jridgewell/sourcemap-codec': 1.4.14 272 | dev: false 273 | 274 | /@types/eslint-scope@3.7.4: 275 | resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} 276 | dependencies: 277 | '@types/eslint': 8.44.2 278 | '@types/estree': 1.0.1 279 | dev: false 280 | 281 | /@types/eslint@8.44.2: 282 | resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==} 283 | dependencies: 284 | '@types/estree': 1.0.1 285 | '@types/json-schema': 7.0.12 286 | dev: false 287 | 288 | /@types/estree@1.0.1: 289 | resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} 290 | dev: false 291 | 292 | /@types/json-schema@7.0.12: 293 | resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} 294 | dev: false 295 | 296 | /@types/node@20.4.8: 297 | resolution: {integrity: sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==} 298 | dev: false 299 | 300 | /@webassemblyjs/ast@1.11.6: 301 | resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} 302 | dependencies: 303 | '@webassemblyjs/helper-numbers': 1.11.6 304 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 305 | dev: false 306 | 307 | /@webassemblyjs/floating-point-hex-parser@1.11.6: 308 | resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} 309 | dev: false 310 | 311 | /@webassemblyjs/helper-api-error@1.11.6: 312 | resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} 313 | dev: false 314 | 315 | /@webassemblyjs/helper-buffer@1.11.6: 316 | resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} 317 | dev: false 318 | 319 | /@webassemblyjs/helper-numbers@1.11.6: 320 | resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} 321 | dependencies: 322 | '@webassemblyjs/floating-point-hex-parser': 1.11.6 323 | '@webassemblyjs/helper-api-error': 1.11.6 324 | '@xtuc/long': 4.2.2 325 | dev: false 326 | 327 | /@webassemblyjs/helper-wasm-bytecode@1.11.6: 328 | resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} 329 | dev: false 330 | 331 | /@webassemblyjs/helper-wasm-section@1.11.6: 332 | resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} 333 | dependencies: 334 | '@webassemblyjs/ast': 1.11.6 335 | '@webassemblyjs/helper-buffer': 1.11.6 336 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 337 | '@webassemblyjs/wasm-gen': 1.11.6 338 | dev: false 339 | 340 | /@webassemblyjs/ieee754@1.11.6: 341 | resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} 342 | dependencies: 343 | '@xtuc/ieee754': 1.2.0 344 | dev: false 345 | 346 | /@webassemblyjs/leb128@1.11.6: 347 | resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} 348 | dependencies: 349 | '@xtuc/long': 4.2.2 350 | dev: false 351 | 352 | /@webassemblyjs/utf8@1.11.6: 353 | resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} 354 | dev: false 355 | 356 | /@webassemblyjs/wasm-edit@1.11.6: 357 | resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} 358 | dependencies: 359 | '@webassemblyjs/ast': 1.11.6 360 | '@webassemblyjs/helper-buffer': 1.11.6 361 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 362 | '@webassemblyjs/helper-wasm-section': 1.11.6 363 | '@webassemblyjs/wasm-gen': 1.11.6 364 | '@webassemblyjs/wasm-opt': 1.11.6 365 | '@webassemblyjs/wasm-parser': 1.11.6 366 | '@webassemblyjs/wast-printer': 1.11.6 367 | dev: false 368 | 369 | /@webassemblyjs/wasm-gen@1.11.6: 370 | resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} 371 | dependencies: 372 | '@webassemblyjs/ast': 1.11.6 373 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 374 | '@webassemblyjs/ieee754': 1.11.6 375 | '@webassemblyjs/leb128': 1.11.6 376 | '@webassemblyjs/utf8': 1.11.6 377 | dev: false 378 | 379 | /@webassemblyjs/wasm-opt@1.11.6: 380 | resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} 381 | dependencies: 382 | '@webassemblyjs/ast': 1.11.6 383 | '@webassemblyjs/helper-buffer': 1.11.6 384 | '@webassemblyjs/wasm-gen': 1.11.6 385 | '@webassemblyjs/wasm-parser': 1.11.6 386 | dev: false 387 | 388 | /@webassemblyjs/wasm-parser@1.11.6: 389 | resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} 390 | dependencies: 391 | '@webassemblyjs/ast': 1.11.6 392 | '@webassemblyjs/helper-api-error': 1.11.6 393 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 394 | '@webassemblyjs/ieee754': 1.11.6 395 | '@webassemblyjs/leb128': 1.11.6 396 | '@webassemblyjs/utf8': 1.11.6 397 | dev: false 398 | 399 | /@webassemblyjs/wast-printer@1.11.6: 400 | resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} 401 | dependencies: 402 | '@webassemblyjs/ast': 1.11.6 403 | '@xtuc/long': 4.2.2 404 | dev: false 405 | 406 | /@xtuc/ieee754@1.2.0: 407 | resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} 408 | dev: false 409 | 410 | /@xtuc/long@4.2.2: 411 | resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 412 | dev: false 413 | 414 | /acorn-import-assertions@1.9.0(acorn@8.10.0): 415 | resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} 416 | peerDependencies: 417 | acorn: ^8 418 | dependencies: 419 | acorn: 8.10.0 420 | dev: false 421 | 422 | /acorn-loose@8.3.0: 423 | resolution: {integrity: sha512-75lAs9H19ldmW+fAbyqHdjgdCrz0pWGXKmnqFoh8PyVd1L2RIb4RzYrSjmopeqv3E1G3/Pimu6GgLlrGbrkF7w==} 424 | engines: {node: '>=0.4.0'} 425 | dependencies: 426 | acorn: 8.10.0 427 | dev: false 428 | 429 | /acorn@8.10.0: 430 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 431 | engines: {node: '>=0.4.0'} 432 | hasBin: true 433 | dev: false 434 | 435 | /ajv-keywords@3.5.2(ajv@6.12.6): 436 | resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} 437 | peerDependencies: 438 | ajv: ^6.9.1 439 | dependencies: 440 | ajv: 6.12.6 441 | dev: false 442 | 443 | /ajv@6.12.6: 444 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 445 | dependencies: 446 | fast-deep-equal: 3.1.3 447 | fast-json-stable-stringify: 2.1.0 448 | json-schema-traverse: 0.4.1 449 | uri-js: 4.4.1 450 | dev: false 451 | 452 | /browserslist@4.21.10: 453 | resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} 454 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 455 | hasBin: true 456 | dependencies: 457 | caniuse-lite: 1.0.30001519 458 | electron-to-chromium: 1.4.485 459 | node-releases: 2.0.13 460 | update-browserslist-db: 1.0.11(browserslist@4.21.10) 461 | dev: false 462 | 463 | /buffer-from@1.1.2: 464 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 465 | dev: false 466 | 467 | /caniuse-lite@1.0.30001519: 468 | resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==} 469 | dev: false 470 | 471 | /chrome-trace-event@1.0.3: 472 | resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} 473 | engines: {node: '>=6.0'} 474 | dev: false 475 | 476 | /commander@2.20.3: 477 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 478 | dev: false 479 | 480 | /electron-to-chromium@1.4.485: 481 | resolution: {integrity: sha512-1ndQ5IBNEnFirPwvyud69GHL+31FkE09gH/CJ6m3KCbkx3i0EVOrjwz4UNxRmN9H8OVHbC6vMRZGN1yCvjSs9w==} 482 | dev: false 483 | 484 | /enhanced-resolve@5.15.0: 485 | resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} 486 | engines: {node: '>=10.13.0'} 487 | dependencies: 488 | graceful-fs: 4.2.11 489 | tapable: 2.2.1 490 | dev: false 491 | 492 | /es-module-lexer@1.3.0: 493 | resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} 494 | dev: false 495 | 496 | /esbuild@0.20.2: 497 | resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} 498 | engines: {node: '>=12'} 499 | hasBin: true 500 | requiresBuild: true 501 | optionalDependencies: 502 | '@esbuild/aix-ppc64': 0.20.2 503 | '@esbuild/android-arm': 0.20.2 504 | '@esbuild/android-arm64': 0.20.2 505 | '@esbuild/android-x64': 0.20.2 506 | '@esbuild/darwin-arm64': 0.20.2 507 | '@esbuild/darwin-x64': 0.20.2 508 | '@esbuild/freebsd-arm64': 0.20.2 509 | '@esbuild/freebsd-x64': 0.20.2 510 | '@esbuild/linux-arm': 0.20.2 511 | '@esbuild/linux-arm64': 0.20.2 512 | '@esbuild/linux-ia32': 0.20.2 513 | '@esbuild/linux-loong64': 0.20.2 514 | '@esbuild/linux-mips64el': 0.20.2 515 | '@esbuild/linux-ppc64': 0.20.2 516 | '@esbuild/linux-riscv64': 0.20.2 517 | '@esbuild/linux-s390x': 0.20.2 518 | '@esbuild/linux-x64': 0.20.2 519 | '@esbuild/netbsd-x64': 0.20.2 520 | '@esbuild/openbsd-x64': 0.20.2 521 | '@esbuild/sunos-x64': 0.20.2 522 | '@esbuild/win32-arm64': 0.20.2 523 | '@esbuild/win32-ia32': 0.20.2 524 | '@esbuild/win32-x64': 0.20.2 525 | dev: false 526 | 527 | /escalade@3.1.1: 528 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 529 | engines: {node: '>=6'} 530 | dev: false 531 | 532 | /eslint-scope@5.1.1: 533 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 534 | engines: {node: '>=8.0.0'} 535 | dependencies: 536 | esrecurse: 4.3.0 537 | estraverse: 4.3.0 538 | dev: false 539 | 540 | /esrecurse@4.3.0: 541 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 542 | engines: {node: '>=4.0'} 543 | dependencies: 544 | estraverse: 5.3.0 545 | dev: false 546 | 547 | /estraverse@4.3.0: 548 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 549 | engines: {node: '>=4.0'} 550 | dev: false 551 | 552 | /estraverse@5.3.0: 553 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 554 | engines: {node: '>=4.0'} 555 | dev: false 556 | 557 | /events@3.3.0: 558 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 559 | engines: {node: '>=0.8.x'} 560 | dev: false 561 | 562 | /fast-deep-equal@3.1.3: 563 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 564 | dev: false 565 | 566 | /fast-json-stable-stringify@2.1.0: 567 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 568 | dev: false 569 | 570 | /glob-to-regexp@0.4.1: 571 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 572 | dev: false 573 | 574 | /graceful-fs@4.2.11: 575 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 576 | dev: false 577 | 578 | /has-flag@4.0.0: 579 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 580 | engines: {node: '>=8'} 581 | dev: false 582 | 583 | /jest-worker@27.5.1: 584 | resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} 585 | engines: {node: '>= 10.13.0'} 586 | dependencies: 587 | '@types/node': 20.4.8 588 | merge-stream: 2.0.0 589 | supports-color: 8.1.1 590 | dev: false 591 | 592 | /json-parse-even-better-errors@2.3.1: 593 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 594 | dev: false 595 | 596 | /json-schema-traverse@0.4.1: 597 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 598 | dev: false 599 | 600 | /loader-runner@4.3.0: 601 | resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} 602 | engines: {node: '>=6.11.5'} 603 | dev: false 604 | 605 | /merge-stream@2.0.0: 606 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 607 | dev: false 608 | 609 | /mime-db@1.52.0: 610 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 611 | engines: {node: '>= 0.6'} 612 | dev: false 613 | 614 | /mime-types@2.1.35: 615 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 616 | engines: {node: '>= 0.6'} 617 | dependencies: 618 | mime-db: 1.52.0 619 | dev: false 620 | 621 | /neo-async@2.6.2: 622 | resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 623 | dev: false 624 | 625 | /node-releases@2.0.13: 626 | resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} 627 | dev: false 628 | 629 | /picocolors@1.0.0: 630 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 631 | dev: false 632 | 633 | /punycode@2.3.0: 634 | resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} 635 | engines: {node: '>=6'} 636 | dev: false 637 | 638 | /randombytes@2.1.0: 639 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 640 | dependencies: 641 | safe-buffer: 5.2.1 642 | dev: false 643 | 644 | /react-dom@19.0.0-rc-cc1ec60d0d-20240607(react@19.0.0-rc-cc1ec60d0d-20240607): 645 | resolution: {integrity: sha512-paspD9kAfKKuURVwKWJ0/g3qYw1DGi9h1k9xQV2iQN9cSVZ4JAOD727yjVLyp1zdzsoygjFfLMtSBdZ+oERYvA==} 646 | peerDependencies: 647 | react: 19.0.0-rc-cc1ec60d0d-20240607 648 | dependencies: 649 | react: 19.0.0-rc-cc1ec60d0d-20240607 650 | scheduler: 0.25.0-rc-cc1ec60d0d-20240607 651 | dev: false 652 | 653 | /react-server-dom-webpack@19.0.0-rc-cc1ec60d0d-20240607(react-dom@19.0.0-rc-cc1ec60d0d-20240607)(react@19.0.0-rc-cc1ec60d0d-20240607)(webpack@5.88.2): 654 | resolution: {integrity: sha512-QVm+y5ELZ6+2kztgtG4N3DHvjuIJVj4b6tRpXUbOc79+Hxz5cvf2RhzRGVTyM6xfb9E1U0tcps1Q2uR8Sa2nEw==} 655 | engines: {node: '>=0.10.0'} 656 | peerDependencies: 657 | react: 19.0.0-rc-cc1ec60d0d-20240607 658 | react-dom: 19.0.0-rc-cc1ec60d0d-20240607 659 | webpack: ^5.59.0 660 | dependencies: 661 | acorn-loose: 8.3.0 662 | neo-async: 2.6.2 663 | react: 19.0.0-rc-cc1ec60d0d-20240607 664 | react-dom: 19.0.0-rc-cc1ec60d0d-20240607(react@19.0.0-rc-cc1ec60d0d-20240607) 665 | webpack: 5.88.2(esbuild@0.20.2) 666 | dev: false 667 | 668 | /react@19.0.0-rc-cc1ec60d0d-20240607: 669 | resolution: {integrity: sha512-q8A0/IdJ2wdHsjDNO1igFcSSFIMqSKmO7oJZtAjxIA9g0klK45Lxt15NQJ7z7cBvgD1r3xRTtQ/MAqnmwYHs1Q==} 670 | engines: {node: '>=0.10.0'} 671 | dev: false 672 | 673 | /safe-buffer@5.2.1: 674 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 675 | dev: false 676 | 677 | /scheduler@0.25.0-rc-cc1ec60d0d-20240607: 678 | resolution: {integrity: sha512-yFVKy6SDJkN2bOJSeH6gNo4+1MTygTZXnLRY5IHvEB6P9+O6WYRWz9PkELLjnl64lQwRgiigwzWQRSMNEboOGQ==} 679 | dev: false 680 | 681 | /schema-utils@3.3.0: 682 | resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} 683 | engines: {node: '>= 10.13.0'} 684 | dependencies: 685 | '@types/json-schema': 7.0.12 686 | ajv: 6.12.6 687 | ajv-keywords: 3.5.2(ajv@6.12.6) 688 | dev: false 689 | 690 | /serialize-javascript@6.0.1: 691 | resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} 692 | dependencies: 693 | randombytes: 2.1.0 694 | dev: false 695 | 696 | /source-map-support@0.5.21: 697 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 698 | dependencies: 699 | buffer-from: 1.1.2 700 | source-map: 0.6.1 701 | dev: false 702 | 703 | /source-map@0.6.1: 704 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 705 | engines: {node: '>=0.10.0'} 706 | dev: false 707 | 708 | /supports-color@8.1.1: 709 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 710 | engines: {node: '>=10'} 711 | dependencies: 712 | has-flag: 4.0.0 713 | dev: false 714 | 715 | /tapable@2.2.1: 716 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 717 | engines: {node: '>=6'} 718 | dev: false 719 | 720 | /terser-webpack-plugin@5.3.9(esbuild@0.20.2)(webpack@5.88.2): 721 | resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} 722 | engines: {node: '>= 10.13.0'} 723 | peerDependencies: 724 | '@swc/core': '*' 725 | esbuild: '*' 726 | uglify-js: '*' 727 | webpack: ^5.1.0 728 | peerDependenciesMeta: 729 | '@swc/core': 730 | optional: true 731 | esbuild: 732 | optional: true 733 | uglify-js: 734 | optional: true 735 | dependencies: 736 | '@jridgewell/trace-mapping': 0.3.18 737 | esbuild: 0.20.2 738 | jest-worker: 27.5.1 739 | schema-utils: 3.3.0 740 | serialize-javascript: 6.0.1 741 | terser: 5.19.2 742 | webpack: 5.88.2(esbuild@0.20.2) 743 | dev: false 744 | 745 | /terser@5.19.2: 746 | resolution: {integrity: sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==} 747 | engines: {node: '>=10'} 748 | hasBin: true 749 | dependencies: 750 | '@jridgewell/source-map': 0.3.5 751 | acorn: 8.10.0 752 | commander: 2.20.3 753 | source-map-support: 0.5.21 754 | dev: false 755 | 756 | /update-browserslist-db@1.0.11(browserslist@4.21.10): 757 | resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} 758 | hasBin: true 759 | peerDependencies: 760 | browserslist: '>= 4.21.0' 761 | dependencies: 762 | browserslist: 4.21.10 763 | escalade: 3.1.1 764 | picocolors: 1.0.0 765 | dev: false 766 | 767 | /uri-js@4.4.1: 768 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 769 | dependencies: 770 | punycode: 2.3.0 771 | dev: false 772 | 773 | /watchpack@2.4.0: 774 | resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} 775 | engines: {node: '>=10.13.0'} 776 | dependencies: 777 | glob-to-regexp: 0.4.1 778 | graceful-fs: 4.2.11 779 | dev: false 780 | 781 | /webpack-sources@3.2.3: 782 | resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} 783 | engines: {node: '>=10.13.0'} 784 | dev: false 785 | 786 | /webpack@5.88.2(esbuild@0.20.2): 787 | resolution: {integrity: sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==} 788 | engines: {node: '>=10.13.0'} 789 | hasBin: true 790 | peerDependencies: 791 | webpack-cli: '*' 792 | peerDependenciesMeta: 793 | webpack-cli: 794 | optional: true 795 | dependencies: 796 | '@types/eslint-scope': 3.7.4 797 | '@types/estree': 1.0.1 798 | '@webassemblyjs/ast': 1.11.6 799 | '@webassemblyjs/wasm-edit': 1.11.6 800 | '@webassemblyjs/wasm-parser': 1.11.6 801 | acorn: 8.10.0 802 | acorn-import-assertions: 1.9.0(acorn@8.10.0) 803 | browserslist: 4.21.10 804 | chrome-trace-event: 1.0.3 805 | enhanced-resolve: 5.15.0 806 | es-module-lexer: 1.3.0 807 | eslint-scope: 5.1.1 808 | events: 3.3.0 809 | glob-to-regexp: 0.4.1 810 | graceful-fs: 4.2.11 811 | json-parse-even-better-errors: 2.3.1 812 | loader-runner: 4.3.0 813 | mime-types: 2.1.35 814 | neo-async: 2.6.2 815 | schema-utils: 3.3.0 816 | tapable: 2.2.1 817 | terser-webpack-plugin: 5.3.9(esbuild@0.20.2)(webpack@5.88.2) 818 | watchpack: 2.4.0 819 | webpack-sources: 3.2.3 820 | transitivePeerDependencies: 821 | - '@swc/core' 822 | - esbuild 823 | - uglify-js 824 | dev: false 825 | -------------------------------------------------------------------------------- /react/.ocamlformat: -------------------------------------------------------------------------------- 1 | ../.ocamlformat -------------------------------------------------------------------------------- /react/api/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name react_api) 3 | (public_name react_server.api) 4 | (modes native melange)) 5 | -------------------------------------------------------------------------------- /react/api/react_api.ml: -------------------------------------------------------------------------------- 1 | (** React interfaces *) 2 | 3 | module type REACT = sig 4 | type dom_element 5 | type 'a nullable 6 | type element 7 | type children = element array 8 | 9 | val array : children -> element 10 | val list : element list -> element 11 | val null : element 12 | val string : string -> element 13 | val int : int -> element 14 | val float : float -> element 15 | val stringf : ('a, unit, string, element) format4 -> 'a 16 | val useState : (unit -> 'state) -> 'state * (('state -> 'state) -> unit) 17 | val useEffect : (unit -> (unit -> unit) option) -> unit 18 | val useEffect1 : (unit -> (unit -> unit) option) -> 'a array -> unit 19 | val startTransition : (unit -> unit) -> unit 20 | 21 | module Context : sig 22 | type 'a t 23 | 24 | val provider : 25 | 'a t -> 26 | ?key:string -> 27 | value:'a -> 28 | children:element -> 29 | unit -> 30 | element 31 | end 32 | 33 | val createContext : 'a -> 'a Context.t 34 | val useContext : 'a Context.t -> 'a 35 | 36 | module Suspense : sig 37 | val make : 38 | ?key:string -> 39 | ?fallback:element -> 40 | children:element -> 41 | unit -> 42 | element 43 | end 44 | 45 | type 'value ref = { mutable current : 'value } 46 | 47 | val useRef : 'value -> 'value ref 48 | 49 | type 'a promise 50 | 51 | val use : 'a promise -> 'a 52 | end 53 | -------------------------------------------------------------------------------- /react/browser/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name react_browser) 3 | (name react_browser) 4 | (wrapped false) 5 | (flags :standard -open Realm) 6 | (modes melange) 7 | (libraries melange.dom realm.browser reason-react melange-fetch) 8 | (preprocess 9 | (pps melange.ppx))) 10 | 11 | (install 12 | (files 13 | (react_browser_runtime.js as browser/react_browser_runtime.js)) 14 | (section lib) 15 | (package react_browser)) 16 | -------------------------------------------------------------------------------- /react/browser/react_browser.ml: -------------------------------------------------------------------------------- 1 | (* module React = React *) 2 | (* module Event = React_browser_event *) 3 | (* module Html_props = React_browser_html_props *) 4 | open Realm 5 | 6 | module React = struct 7 | include React 8 | 9 | external use : 'a Promise.t -> 'a = "use" [@@mel.module "react"] 10 | 11 | external startTransition : (unit -> unit) -> unit = "startTransition" 12 | [@@mel.module "react"] 13 | 14 | external startTransitionAsync : (unit -> unit Promise.t) -> unit 15 | = "startTransition" 16 | [@@mel.module "react"] 17 | 18 | external useOptimistic : 19 | 'state -> ('state -> 'update -> 'state) -> 'state * ('update -> unit) 20 | = "useOptimistic" 21 | [@@mel.module "react"] 22 | end 23 | 24 | module ReactDOM = struct 25 | include ReactDOM 26 | 27 | module Ref = struct 28 | include ReactDOM.Ref 29 | 30 | let useCurrentDomRef () : currentDomRef * domRef = 31 | let ref = React.useRef Js.Nullable.null in 32 | let dom_ref = ReactDOM.Ref.domRef ref in 33 | ref, dom_ref 34 | end 35 | end 36 | 37 | module ReactServerDOM = struct 38 | external createFromFetch : 39 | Fetch.response Promise.t -> React.element Promise.t 40 | = "createFromFetch" 41 | [@@mel.module "react-server-dom-webpack/client"] 42 | end 43 | 44 | module Component_map = React_browser_component_map 45 | 46 | module Router = struct 47 | external navigate : string -> unit = "React_of_caml_navigate" 48 | end 49 | -------------------------------------------------------------------------------- /react/browser/react_browser_component_map.ml: -------------------------------------------------------------------------------- 1 | type exported_component = Js.Json.t Js.Dict.t -> React.element 2 | type t = exported_component Js.Dict.t 3 | 4 | let t = Js.Dict.empty () 5 | 6 | external window : t Js.Dict.t = "window" 7 | 8 | let () = Js.Dict.set window "__exported_components" t 9 | 10 | let register name render = 11 | Js.log ("registering component: " ^ name); 12 | Js.Dict.set t name render 13 | -------------------------------------------------------------------------------- /react/browser/react_browser_runtime.js: -------------------------------------------------------------------------------- 1 | // import this to be defined before we import React/ReactDOM 2 | window.__webpack_require__ = (id) => { 3 | let component = window.__exported_components[id]; 4 | if (component == null) 5 | throw new Error(`unable to resolve client component "${id}"`); 6 | return {__esModule: true, default: component}; 7 | }; 8 | 9 | let React = require('react'); 10 | let ReactDOM = require('react-dom/client'); 11 | let ReactServerDOM = require("react-server-dom-webpack/client"); 12 | 13 | function callServer(id, args) { 14 | throw new Error(`callServer(${id}, ...): not supported yet`); 15 | } 16 | 17 | function Page({loading}) { 18 | let tree = React.use(loading); 19 | return tree; 20 | } 21 | 22 | let loading = null; 23 | let loadingController = null; 24 | let root = null; 25 | 26 | class HTTPChunkedParser { 27 | constructor() { 28 | this.remainder = new Uint8Array(); 29 | this.textDecoder = new TextDecoder(); 30 | } 31 | 32 | transform(chunk, controller) { 33 | let data = new Uint8Array(this.remainder.length + chunk.length); 34 | data.set(this.remainder); 35 | data.set(chunk, this.remainder.length); 36 | 37 | let offset = 0; 38 | while (offset < data.length) { 39 | if (offset + 2 > data.length) break; // need at least 2 bytes 40 | 41 | // find the end of the length line (CRLF) 42 | let endOfLine = data.subarray(offset).indexOf(0x0A); // 0x0A is newline in ASCII 43 | if (endOfLine === -1) break; // need more data 44 | let lengthS = data.subarray(offset, offset + endOfLine); 45 | let length = parseInt(this.textDecoder.decode(lengthS).trim(), 16); 46 | 47 | if (isNaN(length)) { 48 | console.error("Invalid chunk length"); 49 | controller.error("Invalid chunk length"); 50 | return; 51 | } 52 | 53 | // Calculate start and end of the data chunk 54 | let start = offset + endOfLine + 1; 55 | let end = start + length; 56 | if (end > data.length) break; // need more data 57 | 58 | controller.enqueue(data.subarray(start, end)); // extract the data chunk and enqueue it 59 | offset = end + 2; // move the offset past this chunk and the following CRLF 60 | } 61 | this.remainder = data.subarray(offset); 62 | } 63 | 64 | flush(controller) { 65 | if (this.remainder.length > 0) { 66 | // Handle any remaining data that was not a complete chunk 67 | console.error("Incomplete final chunk"); 68 | controller.error("Incomplete final chunk"); 69 | } 70 | } 71 | } 72 | 73 | function fetchRSC(path) { 74 | return fetch(path, { 75 | method: 'GET', 76 | headers: {Accept: 'application/react.component'}, 77 | signal: loadingController.signal, 78 | }).then(response => { 79 | let p = new HTTPChunkedParser(); 80 | let t = new TransformStream(p); 81 | response.body.pipeThrough(t); 82 | response = {body: t.readable}; 83 | return response; 84 | }); 85 | } 86 | 87 | function loadPage(path) { 88 | React.startTransition(() => { 89 | if (loadingController != null) { 90 | loadingController.abort(); 91 | } 92 | loadingController = new AbortController(); 93 | loading = ReactServerDOM.createFromFetch(fetchRSC(path), { callServer }); 94 | if (root === null) 95 | root = ReactDOM.createRoot(document); 96 | root.render( 97 | 98 | 99 | 100 | ); 101 | let {pathname, search} = window.location; 102 | if (pathname + search !== path) 103 | window.history.pushState({}, null, path); 104 | }); 105 | } 106 | 107 | window.React_of_caml_navigate = loadPage; 108 | 109 | function main() { 110 | if (window.React_of_caml_ssr) { 111 | loading = ReactServerDOM.createFromReadableStream( 112 | window.React_of_caml_ssr.stream, 113 | { callServer, } 114 | ); 115 | React.startTransition(() => { 116 | root = ReactDOM.hydrateRoot(document, 117 | 118 | 119 | 120 | ); 121 | }); 122 | } else { 123 | let {pathname, search} = window.location; 124 | loadPage(pathname + search); 125 | } 126 | window.addEventListener("popstate", (event) => { 127 | let {pathname, search} = window.location; 128 | loadPage(pathname + search); 129 | }); 130 | } 131 | 132 | main(); 133 | -------------------------------------------------------------------------------- /react/dream/.ocamlformat: -------------------------------------------------------------------------------- 1 | ../../.ocamlformat -------------------------------------------------------------------------------- /react/dream/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name react_dream) 3 | (public_name react_dream) 4 | (libraries react_server htmlgen lwt dream)) 5 | -------------------------------------------------------------------------------- /react/dream/react_dream.ml: -------------------------------------------------------------------------------- 1 | open ContainersLabels 2 | open Lwt.Infix 3 | open React_server 4 | 5 | let make_script src = 6 | Htmlgen.(node "script" [ "src", s src; "async", b true ] []) 7 | 8 | let make_link href = 9 | Htmlgen.(node "link" [ "href", s href; "rel", s "stylesheet" ] []) 10 | 11 | let html_prelude ~links = 12 | Htmlgen.( 13 | splice ~sep:"\n" 14 | [ 15 | unsafe_raw ""; 16 | node "meta" [ "charset", s "utf-8" ] []; 17 | List.map links ~f:make_link |> splice ~sep:"\n"; 18 | ]) 19 | 20 | let rsc_content_type = "application/react.component" 21 | 22 | module Chunked = struct 23 | type stream = { s : Dream.stream; is_len_encoded : bool } 24 | 25 | let write ?(flush = false) { s; is_len_encoded } data = 26 | (if is_len_encoded then 27 | let len = String.length data in 28 | let len = Printf.sprintf "%x\r\n" len in 29 | Dream.write s len >>= fun () -> 30 | Dream.write s data >>= fun () -> Dream.write s "\r\n" 31 | else Dream.write s data) 32 | >>= fun () -> if flush then Dream.flush s else Lwt.return_unit 33 | 34 | let finish { s; is_len_encoded } = 35 | (if is_len_encoded then Dream.write s "0\r\n\r\n" else Lwt.return ()) 36 | >>= fun () -> Dream.flush s 37 | 38 | let stream ?headers ~is_len_encoded f = 39 | Dream.stream ?headers (fun s -> 40 | let s = { s; is_len_encoded } in 41 | f s >>= fun () -> finish s) 42 | end 43 | 44 | let render ?(enable_client_components = false) ?(enable_ssr = true) 45 | ?(scripts = []) ?(links = []) = 46 | let html_prelude = html_prelude ~links in 47 | let html_scripts = 48 | Htmlgen.(List.map scripts ~f:make_script |> splice ~sep:"\n") 49 | in 50 | fun ui req -> 51 | match Dream.header req "accept" with 52 | | Some accept 53 | when enable_client_components 54 | && String.equal accept rsc_content_type -> 55 | let headers = [ "X-Content-Type-Options", "nosniff" ] in 56 | Chunked.stream ~headers ~is_len_encoded:true @@ fun s -> 57 | render_to_model ui (Chunked.write ~flush:true s) 58 | | _ -> 59 | if enable_ssr then 60 | render_to_html ~render_model:enable_client_components ui 61 | >>= function 62 | | Html_rendering_done { html } -> 63 | Dream.html 64 | Htmlgen.( 65 | splice [ html_prelude; html; html_scripts ] |> to_string) 66 | | Html_rendering_async { html_shell; html_iter } -> 67 | let header = 68 | Htmlgen.( 69 | splice [ html_prelude; html_shell; html_scripts ]) 70 | in 71 | Chunked.stream ~is_len_encoded:false 72 | ~headers:[ "Content-Type", "text/html" ] 73 | @@ fun s -> 74 | let write_html h = 75 | Chunked.write ~flush:true s (Htmlgen.to_string h) 76 | in 77 | write_html header >>= fun () -> html_iter write_html 78 | else 79 | Dream.html 80 | Htmlgen.(splice [ html_prelude; html_scripts ] |> to_string) 81 | -------------------------------------------------------------------------------- /react/dream/react_dream.mli: -------------------------------------------------------------------------------- 1 | (** Serve React applications with Dream. 2 | 3 | {1 React Dream} 4 | 5 | {2 Example} 6 | 7 | A minimal example: 8 | 9 | {[ 10 | let app _req = 11 | jsx.div [| textf "HELLO, WORLD!" |] 12 | 13 | let () = 14 | let links = [ "/static/bundle.css" ] in 15 | let scripts = [ "/static/bundle.js" ] in 16 | Dream.run 17 | @@ Dream.router 18 | [ 19 | Dream.get "/static/**" "./static"; 20 | Dream.get "/" (React_dream.render ~links ~scripts app); 21 | ] 22 | ]} 23 | 24 | *) 25 | 26 | open React_server 27 | 28 | (** {2 Reference} *) 29 | 30 | val render : 31 | ?enable_client_components:bool -> 32 | ?enable_ssr:bool -> 33 | ?scripts:string list -> 34 | ?links:string list -> 35 | React.element -> 36 | Dream.handler 37 | (** [render ~enable_ssr ~scripts ~links app] is a [Dream.handler] which serves 38 | [app], where: 39 | 40 | - [?enable_ssr] enables or disables Server Side Rendering (SSR). It is 41 | enabled by default. 42 | - [?scripts] and [?links] arguments allow to inject [" 124 | {|$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};|} 125 | 126 | let html_chunk idx html = 127 | Htmlgen.( 128 | splice ~sep:"\n" 129 | [ 130 | node "div" 131 | [ "hidden", b true; "id", s (sprintf "S:%x" idx) ] 132 | [ html ]; 133 | unsafe_rawf "" idx idx; 134 | ]) 135 | 136 | let html_suspense inner = 137 | Htmlgen.( 138 | splice [ unsafe_rawf ""; inner; unsafe_rawf "" ]) 139 | 140 | let html_suspense_placeholder (fallback : Htmlgen.t) idx = 141 | Htmlgen.( 142 | html_suspense 143 | (splice 144 | [ 145 | unsafe_rawf {||} idx; fallback; 146 | ])) 147 | 148 | let splice = Htmlgen.splice ~sep:"" 149 | end 150 | 151 | module Emit_model = struct 152 | let html_start = 153 | Htmlgen.unsafe_rawf 154 | {| |} 166 | 167 | let html_model model = 168 | let chunk = Render_to_model.chunk_to_string model in 169 | Htmlgen.unsafe_rawf 170 | "" 171 | (Htmlgen.single_quote_escape chunk) 172 | 173 | let html_end = 174 | Htmlgen.unsafe_rawf 175 | "" 176 | end 177 | 178 | let rec client_to_html t = function 179 | | React_model.El_null -> Lwt.return (Htmlgen.text "") 180 | | El_text s -> Lwt.return (Htmlgen.text s) 181 | | El_frag els -> client_to_html_many t els 182 | | El_context (key, value, children) -> 183 | let t = Fiber.update_ctx t key value in 184 | client_to_html t children 185 | | El_html { tag_name; key = _; props; children = None } -> 186 | Lwt.return (Htmlgen.node tag_name props []) 187 | | El_html 188 | { 189 | tag_name; 190 | key = _; 191 | props; 192 | children = Some (Html_children children); 193 | } -> 194 | client_to_html t children >|= fun children -> 195 | Htmlgen.node tag_name props [ children ] 196 | | El_html 197 | { 198 | tag_name; 199 | key = _; 200 | props; 201 | children = Some (Html_children_raw { __html }); 202 | } -> 203 | Lwt.return 204 | (Htmlgen.node tag_name props [ Htmlgen.unsafe_raw __html ]) 205 | | El_thunk f -> 206 | let rec wait () = 207 | match Fiber.with_ctx t f with 208 | | exception React_model.Suspend (Any_promise promise) -> 209 | promise >>= fun _ -> wait () 210 | | v, batch -> 211 | Lwt.both (client_to_html t v) (Fiber.emit_batch t batch) 212 | >|= fst 213 | in 214 | wait () 215 | | El_async_thunk _ -> failwith "async component in client mode" 216 | | El_suspense { children; fallback; key = _ } -> ( 217 | match children with 218 | | El_null -> Lwt.return (Emit_html.html_suspense Htmlgen.empty) 219 | | _ -> 220 | client_to_html t fallback >>= fun fallback -> 221 | Fiber.fork t @@ fun t -> 222 | let idx = Fiber.use_idx t in 223 | let async = 224 | client_to_html t children >|= Emit_html.html_chunk idx 225 | in 226 | `Fork (async, Emit_html.html_suspense_placeholder fallback idx)) 227 | | El_client_thunk 228 | { import_module = _; import_name = _; props = _; thunk } -> 229 | client_to_html t thunk 230 | 231 | and client_to_html_many t els : Htmlgen.t Lwt.t = 232 | Array.to_list els 233 | |> Lwt_list.map_p (client_to_html t) 234 | >|= Emit_html.splice 235 | 236 | let rec server_to_html ~render_model t = function 237 | | React_model.El_null -> Lwt.return (Htmlgen.empty, Render_to_model.null) 238 | | El_text s -> 239 | Lwt.return 240 | ( Htmlgen.text s, 241 | if not render_model then Render_to_model.null 242 | else Render_to_model.text s ) 243 | | El_frag els -> server_to_html_many ~render_model t els 244 | | El_context _ -> 245 | failwith "react context is not supported in server environment" 246 | | El_html 247 | { tag_name; key; props; children = Some (Html_children children) } 248 | -> 249 | server_to_html ~render_model t children >|= fun (html, children) -> 250 | ( Htmlgen.node tag_name props [ html ], 251 | if not render_model then Render_to_model.null 252 | else 253 | Render_to_model.node ~tag_name ~key 254 | ~props:(props :> (string * json) list) 255 | (Some children) ) 256 | | El_html 257 | { 258 | tag_name; 259 | key; 260 | props; 261 | children = Some (Html_children_raw { __html }); 262 | } -> 263 | Lwt.return 264 | ( Htmlgen.node tag_name props [ Htmlgen.unsafe_raw __html ], 265 | if not render_model then Render_to_model.null 266 | else 267 | let props = (props :> (string * json) list) in 268 | let props = 269 | ( "dangerouslySetInnerHTML", 270 | `Assoc [ "__html", `String __html ] ) 271 | :: props 272 | in 273 | Render_to_model.node ~tag_name ~key ~props None ) 274 | | El_html { tag_name; key; props; children = None } -> 275 | Lwt.return 276 | ( Htmlgen.node tag_name props [], 277 | if not render_model then Render_to_model.null 278 | else 279 | Render_to_model.node ~tag_name ~key 280 | ~props:(props :> (string * json) list) 281 | None ) 282 | | El_thunk f -> 283 | let tree, _reqs = Fiber.with_ctx t f in 284 | (* NOTE: ignoring [_reqs] here as server data requests won't be replayed 285 | on client, while we still want to allow to use Remote library *) 286 | server_to_html ~render_model t tree 287 | | El_async_thunk f -> 288 | Fiber.with_ctx_async t f >>= fun (tree, _reqs) -> 289 | (* NOTE: ignoring [_reqs] here as server data requests won't be replayed 290 | on client, while we still want to allow to use Remote library *) 291 | server_to_html ~render_model t tree 292 | | El_suspense { children; fallback; key } -> ( 293 | server_to_html ~render_model t fallback 294 | >>= fun (fallback, fallback_model) -> 295 | Fiber.fork t @@ fun t -> 296 | let promise = server_to_html ~render_model t children in 297 | match Lwt.state promise with 298 | | Sleep -> 299 | let idx = Fiber.use_idx t in 300 | let html_async = 301 | promise >|= fun (html, model) -> 302 | let html = [ Emit_html.html_chunk idx html ] in 303 | let html = 304 | if not render_model then html 305 | else 306 | Emit_model.html_model (idx, Render_to_model.C_value model) 307 | :: html 308 | in 309 | Htmlgen.splice html 310 | in 311 | let html_sync = 312 | ( Emit_html.html_suspense_placeholder fallback idx, 313 | if not render_model then Render_to_model.null 314 | else 315 | Render_to_model.suspense_placeholder ~key 316 | ~fallback:fallback_model idx ) 317 | in 318 | `Fork (html_async, html_sync) 319 | | Return (html, model) -> 320 | `Sync 321 | ( Emit_html.html_suspense html, 322 | if not render_model then Render_to_model.null 323 | else 324 | Render_to_model.suspense ~key ~fallback:fallback_model 325 | model ) 326 | | Fail exn -> `Fail exn) 327 | | El_client_thunk { import_module; import_name; props; thunk } -> 328 | let props = 329 | Lwt_list.map_p 330 | (fun (name, jsony) -> 331 | match jsony with 332 | | React_model.Element element -> 333 | server_to_html ~render_model t element 334 | >|= fun (_html, model) -> name, model 335 | | Promise (promise, value_to_json) -> 336 | Fiber.fork t @@ fun t -> 337 | let idx = Fiber.use_idx t in 338 | let sync = 339 | ( name, 340 | if not render_model then Render_to_model.null 341 | else Render_to_model.promise_value idx ) 342 | in 343 | let async = 344 | promise >|= fun value -> 345 | let json = value_to_json value in 346 | Emit_model.html_model (idx, C_value json) 347 | in 348 | `Fork (async, sync) 349 | | Json json -> Lwt.return (name, json)) 350 | props 351 | in 352 | let html = client_to_html t thunk in 353 | (* NOTE: this Lwt.pause () is important as we resolve client component in 354 | an async way we need to suspend above, otherwise React.js runtime won't work *) 355 | Lwt.pause () >>= fun () -> 356 | Lwt.both html props >|= fun (html, props) -> 357 | let model = 358 | if not render_model then Render_to_model.null 359 | else 360 | let idx = Fiber.use_idx t in 361 | let ref = Render_to_model.ref ~import_module ~import_name in 362 | Fiber.emit_html t (Emit_model.html_model (idx, C_ref ref)); 363 | Render_to_model.node ~tag_name:(sprintf "$%x" idx) ~key:None 364 | ~props None 365 | in 366 | html, model 367 | 368 | and server_to_html_many ~render_model finished els = 369 | Array.to_list els 370 | |> Lwt_list.map_p (server_to_html ~render_model finished) 371 | >|= List.split 372 | >|= fun (htmls, model) -> 373 | Emit_html.splice htmls, Render_to_model.list model 374 | 375 | type html_rendering = 376 | | Html_rendering_done of { html : Htmlgen.t } 377 | | Html_rendering_async of { 378 | html_shell : Htmlgen.t; 379 | html_iter : (Htmlgen.t -> unit Lwt.t) -> unit Lwt.t; 380 | } 381 | 382 | let render ~render_model el = 383 | let html = 384 | Fiber.root @@ fun (t, idx) -> 385 | server_to_html ~render_model t el >|= fun (html, model) -> 386 | if not render_model then html 387 | else 388 | Htmlgen.splice 389 | [ 390 | Emit_model.html_model (idx, Render_to_model.C_value model); html; 391 | ] 392 | in 393 | html >|= fun (html_shell, async) -> 394 | match async with 395 | | None -> 396 | let html = 397 | if not render_model then html_shell 398 | else 399 | Htmlgen.splice 400 | [ Emit_model.html_start; html_shell; Emit_model.html_end ] 401 | in 402 | Html_rendering_done { html } 403 | | Some async -> 404 | let html_iter f = 405 | Lwt_stream.iter_s f async >>= fun () -> 406 | if not render_model then Lwt.return () else f Emit_model.html_end 407 | in 408 | let html_shell = 409 | if not render_model then 410 | Htmlgen.splice [ Emit_html.html_rc; html_shell ] 411 | else 412 | Htmlgen.( 413 | splice 414 | [ Emit_model.html_start; Emit_html.html_rc; html_shell ]) 415 | in 416 | Html_rendering_async { html_shell; html_iter } 417 | -------------------------------------------------------------------------------- /react/server/render_to_model.ml: -------------------------------------------------------------------------------- 1 | open! Import 2 | open Lwt.Infix 3 | 4 | type model = json 5 | 6 | let text text = `String text 7 | let null = `Null 8 | let list xs = `List xs 9 | 10 | let node ~tag_name ~key ~props children : model = 11 | let key = match key with None -> `Null | Some key -> `String key in 12 | let props = 13 | match children with 14 | | None -> props 15 | | Some children -> ("children", children) :: props 16 | in 17 | `List [ `String "$"; `String tag_name; key; `Assoc props ] 18 | 19 | let suspense ~key ~fallback children = 20 | node ~tag_name:"$Sreact.suspense" ~key 21 | ~props:[ "fallback", fallback ] 22 | (Some children) 23 | 24 | let lazy_value idx = `String (sprintf "$L%x" idx) 25 | let promise_value idx = `String (sprintf "$@%x" idx) 26 | 27 | let suspense_placeholder ~key ~fallback idx = 28 | suspense ~key ~fallback (lazy_value idx) 29 | 30 | let ref ~import_module ~import_name = 31 | `List 32 | [ 33 | `String import_module (* id *); 34 | `List [] (* chunks *); 35 | `String import_name (* name *); 36 | ] 37 | 38 | type chunk = C_value of model | C_ref of model 39 | 40 | let chunk_to_string = function 41 | | idx, C_ref ref -> 42 | let buf = Buffer.create 256 in 43 | Buffer.add_string buf (sprintf "%x:I" idx); 44 | Yojson.Basic.write_json buf ref; 45 | Buffer.add_char buf '\n'; 46 | Buffer.contents buf 47 | | idx, C_value model -> 48 | let buf = Buffer.create (4 * 1024) in 49 | Buffer.add_string buf (sprintf "%x:" idx); 50 | Yojson.Basic.write_json buf model; 51 | Buffer.add_char buf '\n'; 52 | Buffer.contents buf 53 | 54 | type ctx = { 55 | mutable idx : int; 56 | mutable pending : int; 57 | push : string option -> unit; 58 | remote_ctx : Remote.Context.t; 59 | } 60 | 61 | let use_idx ctx = 62 | ctx.idx <- ctx.idx + 1; 63 | ctx.idx 64 | 65 | let push ctx chunk = ctx.push (Some (chunk_to_string chunk)) 66 | let close ctx = ctx.push None 67 | 68 | let rec to_model ctx idx el = 69 | let rec to_model' : React_model.element -> model = function 70 | | El_null -> `Null 71 | | El_text s -> `String s 72 | | El_frag els -> `List (Array.map els ~f:to_model' |> Array.to_list) 73 | | El_context _ -> 74 | failwith "react context is not supported in server environment" 75 | | El_html { tag_name; key; props; children } -> 76 | let props = (props :> (string * json) list) in 77 | let children, props = 78 | match children with 79 | | None -> None, props 80 | | Some (Html_children children) -> 81 | Some (children |> to_model'), props 82 | | Some (Html_children_raw { __html }) -> 83 | ( None, 84 | ( "dangerouslySetInnerHTML", 85 | `Assoc [ "__html", `String __html ] ) 86 | :: props ) 87 | in 88 | node ~tag_name ~key ~props children 89 | | El_suspense { children; fallback; key } -> 90 | let fallback = to_model' fallback in 91 | suspense ~key ~fallback (to_model' children) 92 | | El_thunk f -> 93 | let tree, _reqs = Remote.Context.with_ctx ctx.remote_ctx f in 94 | to_model' tree 95 | | El_async_thunk f -> ( 96 | let tree = Remote.Context.with_ctx_async ctx.remote_ctx f in 97 | match Lwt.state tree with 98 | | Lwt.Return (tree, _reqs) -> to_model' tree 99 | | Lwt.Fail exn -> raise exn 100 | | Lwt.Sleep -> 101 | let idx = use_idx ctx in 102 | ctx.pending <- ctx.pending + 1; 103 | Lwt.async (fun () -> 104 | tree >|= fun (tree, _reqs) -> 105 | ctx.pending <- ctx.pending - 1; 106 | to_model ctx idx tree); 107 | lazy_value idx) 108 | | El_client_thunk { import_module; import_name; props; thunk = _ } -> 109 | let idx = use_idx ctx in 110 | let ref = ref ~import_module ~import_name in 111 | push ctx (idx, C_ref ref); 112 | let props = 113 | List.map props ~f:(function 114 | | name, React_model.Element element -> name, to_model' element 115 | | name, Promise (promise, value_to_json) -> ( 116 | match Lwt.state promise with 117 | | Return value -> 118 | let idx = use_idx ctx in 119 | let json = value_to_json value in 120 | (* NOTE: important to yield a chunk here for React.js *) 121 | push ctx (idx, C_value json); 122 | name, promise_value idx 123 | | Sleep -> 124 | let idx = use_idx ctx in 125 | ctx.pending <- ctx.pending + 1; 126 | Lwt.async (fun () -> 127 | promise >|= fun value -> 128 | let json = value_to_json value in 129 | ctx.pending <- ctx.pending - 1; 130 | push ctx (idx, C_value json); 131 | if ctx.pending = 0 then close ctx); 132 | name, promise_value idx 133 | | Fail exn -> raise exn) 134 | | name, Json json -> name, json) 135 | in 136 | node ~tag_name:(sprintf "$%x" idx) ~key:None ~props None 137 | in 138 | push ctx (idx, C_value (to_model' el)); 139 | if ctx.pending = 0 then close ctx 140 | 141 | let render el on_chunk = 142 | let rendering, push = Lwt_stream.create () in 143 | let ctx = 144 | { push; pending = 0; idx = 0; remote_ctx = Remote.Context.create () } 145 | in 146 | to_model ctx ctx.idx el; 147 | Lwt_stream.iter_s on_chunk rendering >|= fun () -> 148 | match Lwt.state (Remote.Context.wait ctx.remote_ctx) with 149 | | Sleep -> 150 | (* NOTE: this can happen if a promise which was fired a component wasn't 151 | waited for *) 152 | prerr_endline "some promises are not yet finished" 153 | | _ -> () 154 | -------------------------------------------------------------------------------- /react/server/render_to_model.mli: -------------------------------------------------------------------------------- 1 | open! Import 2 | 3 | val node : 4 | tag_name:string -> 5 | key:string option -> 6 | props:(string * json) list -> 7 | json option -> 8 | json 9 | 10 | val ref : import_module:string -> import_name:string -> json 11 | val null : json 12 | val text : string -> json 13 | val list : json list -> json 14 | val suspense : key:string option -> fallback:json -> json -> json 15 | 16 | val suspense_placeholder : 17 | key:string option -> fallback:json -> int -> json 18 | 19 | val promise_value : int -> json 20 | val lazy_value : int -> json 21 | 22 | type chunk = C_value of json | C_ref of json 23 | 24 | val chunk_to_string : int * chunk -> string 25 | 26 | val render : React_model.element -> (string -> unit Lwt.t) -> unit Lwt.t 27 | (** Render React elements into a serialized model. *) 28 | -------------------------------------------------------------------------------- /react_browser.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: ["Andrey Popp"] 4 | authors: ["Andrey Popp"] 5 | license: "LICENSE" 6 | homepage: "https://github.com/andreypopp/reactor" 7 | bug-reports: "https://github.com/andreypopp/reactor/issues" 8 | depends: [ 9 | "ocamlformat" {with-test} 10 | "ocaml" {>= "4.14"} 11 | "melange" 12 | "dune" {>= "3.16"} 13 | "reason-react" 14 | "remote" 15 | "realm" 16 | "ppxlib" 17 | "odoc" {with-doc} 18 | ] 19 | build: [ 20 | ["dune" "subst"] {dev} 21 | [ 22 | "dune" 23 | "build" 24 | "-p" 25 | name 26 | "-j" 27 | jobs 28 | "--promote-install-files=false" 29 | "@install" 30 | "@runtest" {with-test} 31 | "@doc" {with-doc} 32 | ] 33 | ["dune" "install" "-p" name "--create-install-files" name] 34 | ] 35 | dev-repo: "git+https://github.com/andreypopp/reactor.git" 36 | -------------------------------------------------------------------------------- /react_dream.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: ["Andrey Popp"] 4 | authors: ["Andrey Popp"] 5 | license: "LICENSE" 6 | homepage: "https://github.com/andreypopp/reactor" 7 | bug-reports: "https://github.com/andreypopp/reactor/issues" 8 | depends: [ 9 | "ocaml" {>= "4.14"} 10 | "dune" {>= "3.16"} 11 | "lwt" 12 | "dream" 13 | "htmlgen" 14 | "react_server" 15 | "odoc" {with-doc} 16 | ] 17 | build: [ 18 | ["dune" "subst"] {dev} 19 | [ 20 | "dune" 21 | "build" 22 | "-p" 23 | name 24 | "-j" 25 | jobs 26 | "--promote-install-files=false" 27 | "@install" 28 | "@runtest" {with-test} 29 | "@doc" {with-doc} 30 | ] 31 | ["dune" "install" "-p" name "--create-install-files" name] 32 | ] 33 | dev-repo: "git+https://github.com/andreypopp/reactor.git" 34 | -------------------------------------------------------------------------------- /react_server.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: ["Andrey Popp"] 4 | authors: ["Andrey Popp"] 5 | license: "LICENSE" 6 | homepage: "https://github.com/andreypopp/reactor" 7 | bug-reports: "https://github.com/andreypopp/reactor/issues" 8 | depends: [ 9 | "ocamlformat" {with-test} 10 | "ocaml" {>= "4.14"} 11 | "dune" {>= "3.16"} 12 | "remote" 13 | "realm" 14 | "htmlgen" 15 | "containers" 16 | "lwt" 17 | "ppxlib" 18 | "yojson" 19 | "odoc" {with-doc} 20 | ] 21 | build: [ 22 | ["dune" "subst"] {dev} 23 | [ 24 | "dune" 25 | "build" 26 | "-p" 27 | name 28 | "-j" 29 | jobs 30 | "--promote-install-files=false" 31 | "@install" 32 | "@runtest" {with-test} 33 | "@doc" {with-doc} 34 | ] 35 | ["dune" "install" "-p" name "--create-install-files" name] 36 | ] 37 | dev-repo: "git+https://github.com/andreypopp/reactor.git" 38 | -------------------------------------------------------------------------------- /realm.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: ["Andrey Popp"] 4 | authors: ["Andrey Popp"] 5 | license: "LICENSE" 6 | homepage: "https://github.com/andreypopp/reactor" 7 | bug-reports: "https://github.com/andreypopp/reactor/issues" 8 | depends: [ 9 | "ocaml" {>= "4.14"} 10 | "melange" 11 | "dune" {>= "3.16"} 12 | "lwt" 13 | "yojson" 14 | "odoc" {with-doc} 15 | ] 16 | build: [ 17 | ["dune" "subst"] {dev} 18 | [ 19 | "dune" 20 | "build" 21 | "-p" 22 | name 23 | "-j" 24 | jobs 25 | "--promote-install-files=false" 26 | "@install" 27 | "@runtest" {with-test} 28 | "@doc" {with-doc} 29 | ] 30 | ["dune" "install" "-p" name "--create-install-files" name] 31 | ] 32 | dev-repo: "git+https://github.com/andreypopp/reactor.git" 33 | -------------------------------------------------------------------------------- /realm/.ocamlformat: -------------------------------------------------------------------------------- 1 | ../.ocamlformat -------------------------------------------------------------------------------- /realm/browser/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name realm.browser) 3 | (name realm_browser) 4 | (wrapped false) 5 | (preprocess 6 | (pps melange.ppx)) 7 | (modes melange)) 8 | -------------------------------------------------------------------------------- /realm/browser/realm.ml: -------------------------------------------------------------------------------- 1 | module Promise = struct 2 | type 'a t = 'a Js.Promise.t 3 | 4 | let return v = Js.Promise.resolve v 5 | let ( let* ) v e = Js.Promise.then_ e v 6 | 7 | let sleep sec = 8 | Js.Promise.make @@ fun ~resolve ~reject:_ -> 9 | let unit = () in 10 | ignore 11 | (Js.Global.setIntervalFloat 12 | ~f:(fun () -> (resolve unit [@u])) 13 | (sec *. 1000.)) 14 | end 15 | 16 | module Json = struct 17 | type t = Js.Json.t 18 | 19 | let to_json t = t 20 | let of_json t = t 21 | 22 | module To_json = struct 23 | external string_to_json : string -> t = "%identity" 24 | external bool_to_json : bool -> t = "%identity" 25 | external int_to_json : int -> t = "%identity" 26 | external float_to_json : float -> t = "%identity" 27 | 28 | let unit_to_json () : t = Obj.magic Js.null 29 | 30 | let list_to_json v_to_json vs : t = 31 | let vs = Array.of_list vs in 32 | let vs : Js.Json.t array = Array.map v_to_json vs in 33 | Obj.magic vs 34 | 35 | let option_to_json v_to_json v : t = 36 | match v with None -> Obj.magic Js.null | Some v -> v_to_json v 37 | end 38 | 39 | exception Of_json_error of string 40 | 41 | let of_json_error msg = raise (Of_json_error msg) 42 | 43 | module Of_json = struct 44 | let string_of_json (json : t) : string = 45 | if Js.typeof json = "string" then (Obj.magic json : string) 46 | else of_json_error "expected a string" 47 | 48 | let bool_of_json (json : t) : bool = 49 | if Js.typeof json = "boolean" then (Obj.magic json : bool) 50 | else of_json_error "expected a boolean" 51 | 52 | let is_int value = 53 | Js.Float.isFinite value && Js.Math.floor_float value == value 54 | 55 | let int_of_json (json : t) : int = 56 | if Js.typeof json = "number" then 57 | let v = (Obj.magic json : float) in 58 | if is_int v then (Obj.magic v : int) 59 | else of_json_error "expected an integer" 60 | else of_json_error "expected a boolean" 61 | 62 | let unit_of_json (json : t) = 63 | if (Obj.magic json : 'a Js.null) == Js.null then () 64 | else of_json_error "expected null" 65 | 66 | let option_of_json v_of_json (json : t) = 67 | if (Obj.magic json : 'a Js.null) == Js.null then None 68 | else Some (v_of_json json) 69 | 70 | let list_of_json v_of_json (json : t) = 71 | if Js.Array.isArray json then 72 | let json = (Obj.magic json : Js.Json.t array) in 73 | Array.to_list (Array.map v_of_json json) 74 | else of_json_error "expected a JSON array" 75 | end 76 | end 77 | 78 | include Json.To_json 79 | include Json.Of_json 80 | -------------------------------------------------------------------------------- /realm/native/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name realm.native) 3 | (name realm_native) 4 | (wrapped false) 5 | (libraries lwt lwt.unix yojson) 6 | (modes native)) 7 | -------------------------------------------------------------------------------- /realm/native/realm.ml: -------------------------------------------------------------------------------- 1 | module Promise = struct 2 | type 'a t = 'a Lwt.t 3 | 4 | let return v = Lwt.return v 5 | let ( let* ) v e = Lwt.bind v e 6 | let sleep = Lwt_unix.sleep 7 | end 8 | 9 | module Json = struct 10 | type t = Yojson.Basic.t 11 | 12 | let to_json t = t 13 | let of_json t = t 14 | 15 | module To_json = struct 16 | let string_to_json v = `String v 17 | let bool_to_json v = `Bool v 18 | let int_to_json v = `Int v 19 | let unit_to_json () = `Null 20 | let list_to_json v_to_json vs = `List (List.map v_to_json vs) 21 | 22 | let option_to_json v_to_json = function 23 | | None -> `Null 24 | | Some v -> v_to_json v 25 | end 26 | 27 | exception Of_json_error of string 28 | 29 | let of_json_error msg = raise (Of_json_error msg) 30 | 31 | module Of_json = struct 32 | let string_of_json = Yojson.Basic.Util.to_string 33 | let bool_of_json = Yojson.Basic.Util.to_bool 34 | let int_of_json = Yojson.Basic.Util.to_int 35 | 36 | let unit_of_json = function 37 | | `Null -> () 38 | | _ -> of_json_error "expected null" 39 | 40 | let option_of_json v_of_json = Yojson.Basic.Util.to_option v_of_json 41 | let list_of_json v_of_json = Yojson.Basic.Util.to_list v_of_json 42 | end 43 | end 44 | 45 | include Json.To_json 46 | include Json.Of_json 47 | -------------------------------------------------------------------------------- /remote.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: ["Andrey Popp"] 4 | authors: ["Andrey Popp"] 5 | license: "LICENSE" 6 | homepage: "https://github.com/andreypopp/reactor" 7 | bug-reports: "https://github.com/andreypopp/reactor/issues" 8 | depends: [ 9 | "ocaml" {>= "4.14"} 10 | "melange" 11 | "dune" {>= "3.16"} 12 | "melange-fetch" 13 | "containers" 14 | "ppxlib" 15 | "htmlgen" 16 | "hmap" 17 | "realm" 18 | "ppx_deriving_router" 19 | "yojson" 20 | "odoc" {with-doc} 21 | ] 22 | build: [ 23 | ["dune" "subst"] {dev} 24 | [ 25 | "dune" 26 | "build" 27 | "-p" 28 | name 29 | "-j" 30 | jobs 31 | "--promote-install-files=false" 32 | "@install" 33 | "@runtest" {with-test} 34 | "@doc" {with-doc} 35 | ] 36 | ["dune" "install" "-p" name "--create-install-files" name] 37 | ] 38 | dev-repo: "git+https://github.com/andreypopp/reactor.git" 39 | -------------------------------------------------------------------------------- /remote/.ocamlformat: -------------------------------------------------------------------------------- 1 | ../.ocamlformat -------------------------------------------------------------------------------- /remote/browser/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name remote_browser) 3 | (wrapped false) 4 | (public_name remote.browser) 5 | (modes melange) 6 | (flags :standard -open Realm) 7 | (libraries 8 | melange-fetch 9 | realm.browser 10 | ppx_deriving_json.browser_runtime 11 | ppx_deriving_router.browser_runtime) 12 | (preprocess 13 | (pps melange.ppx))) 14 | -------------------------------------------------------------------------------- /remote/browser/remote.ml: -------------------------------------------------------------------------------- 1 | module Witness = Ppx_deriving_router_runtime.Witness 2 | module Promise = Realm.Promise 3 | 4 | module Tag : sig 5 | type t 6 | 7 | val make : string -> t 8 | val to_string : t -> string 9 | end = struct 10 | type t = string 11 | 12 | let make x = x 13 | let to_string x = x 14 | end 15 | 16 | type 'a req = { 17 | path : string; 18 | input : string Lazy.t; 19 | decode_response : Fetch.Response.t -> 'a Promise.t; 20 | witness : 'a Witness.t; 21 | tags : Tag.t list; 22 | } 23 | 24 | module Cache : sig 25 | val cached : 'a req -> (unit -> string Promise.t) -> 'a Promise.t 26 | val invalidate : 'a req -> unit 27 | val invalidate_tag : Tag.t -> unit 28 | end = struct 29 | module Record : sig 30 | type t = { 31 | mutable json : string Promise.t Js.nullable; 32 | mutable data : cached_data Js.nullable; 33 | mutable tags : string list; 34 | } 35 | 36 | and cached_data = 37 | | Cached_data : 'a Witness.t * 'a Promise.t -> cached_data 38 | 39 | val invalidate : t -> unit 40 | end = struct 41 | type t = { 42 | mutable json : string Promise.t Js.nullable; 43 | mutable data : cached_data Js.nullable; 44 | mutable tags : string list; 45 | } 46 | (** Cache record. We split into json and data fields b/c SSR payload 47 | contains only json data, so we should be able to decode such "partial" 48 | cache into a properly typed value. *) 49 | 50 | and cached_data = 51 | | Cached_data : 'a Witness.t * 'a Promise.t -> cached_data 52 | 53 | let invalidate t = 54 | t.json <- Js.Nullable.null; 55 | t.data <- Js.Nullable.null 56 | end 57 | 58 | external new_Response : string -> Fetch.response = "Response" 59 | [@@mel.new] 60 | 61 | let cached_data req (data : string Promise.t) = 62 | Promise.( 63 | let data = 64 | let* data = data in 65 | let response = new_Response data in 66 | req.decode_response response 67 | in 68 | data, Record.Cached_data (req.witness, data)) 69 | 70 | type window 71 | 72 | external window : window = "window" 73 | 74 | module By_req : sig 75 | type t = Record.t Js.Dict.t Js.Dict.t 76 | 77 | val find : t -> _ req -> Record.t Js.dict * string * Record.t option 78 | val invalidate : t -> _ req -> unit 79 | val instance : t Lazy.t 80 | val iter : f:(Record.t -> unit) -> t -> unit 81 | end = struct 82 | type t = Record.t Js.Dict.t Js.Dict.t 83 | 84 | let find t req = 85 | let t = 86 | match Js.Dict.get t req.path with 87 | | None -> 88 | let cache' = Js.Dict.empty () in 89 | Js.Dict.set t req.path cache'; 90 | cache' 91 | | Some cache' -> cache' 92 | in 93 | let k = Lazy.force req.input in 94 | let v = Js.Dict.get t k in 95 | t, k, v 96 | 97 | let invalidate t req = 98 | match Js.Dict.get t req.path with 99 | | None -> () 100 | | Some t -> ( 101 | match Js.Dict.get t (Lazy.force req.input) with 102 | | None -> () 103 | | Some record -> Record.invalidate record) 104 | 105 | external instance : t Js.Undefined.t = "window.__Remote_cache" 106 | 107 | let instance = 108 | lazy 109 | (match Js.Undefined.toOption instance with 110 | | None -> 111 | let t = Js.Dict.empty () in 112 | Js.Dict.set 113 | (Obj.magic window : t Js.Dict.t) 114 | "__Remote_cache" t; 115 | t 116 | | Some t -> t) 117 | 118 | let iter ~f t = 119 | Array.iter 120 | (fun k -> 121 | let t = Js.Dict.unsafeGet t k in 122 | Array.iter 123 | (fun k -> 124 | let v = Js.Dict.unsafeGet t k in 125 | f v) 126 | (Js.Dict.keys t)) 127 | (Js.Dict.keys t) 128 | end 129 | 130 | module By_tag : sig 131 | val set : prev_tags:string list -> 'a req -> Record.t -> unit 132 | val invalidate : Tag.t -> unit 133 | end = struct 134 | type t = By_req.t Js.Dict.t 135 | 136 | external instance : t Js.Undefined.t = "window.__Remote_cache_by_tag" 137 | 138 | let instance = 139 | lazy 140 | (match Js.Undefined.toOption instance with 141 | | None -> 142 | let t = Js.Dict.empty () in 143 | Js.Dict.set 144 | (Obj.magic window : t Js.Dict.t) 145 | "__Remote_cache_by_tag" t; 146 | t 147 | | Some t -> t) 148 | 149 | let for_tag t tag = 150 | match Js.Dict.get t tag with 151 | | None -> 152 | let cache = Js.Dict.empty () in 153 | Js.Dict.set t tag cache; 154 | cache 155 | | Some cache -> cache 156 | 157 | let set ~prev_tags (req : _ req) record = 158 | let by_tag = Lazy.force instance in 159 | let tags = List.map Tag.to_string req.tags in 160 | List.iter 161 | (fun tag -> 162 | if not (List.mem tag tags) then 163 | let cache = for_tag by_tag tag in 164 | By_req.invalidate cache req) 165 | prev_tags; 166 | List.iter 167 | (fun tag -> 168 | let cache = for_tag by_tag tag in 169 | let cache, key, _ = By_req.find cache req in 170 | Js.Dict.set cache key record; 171 | ()) 172 | tags 173 | 174 | let invalidate tag = 175 | let t = Lazy.force instance in 176 | match Js.Dict.get t (Tag.to_string tag) with 177 | | None -> () 178 | | Some cache -> By_req.iter cache ~f:Record.invalidate 179 | end 180 | 181 | let cached (type a) (req : a req) (f : unit -> string Promise.t) : 182 | a Promise.t = 183 | let cache, key, record = 184 | By_req.find (Lazy.force By_req.instance) req 185 | in 186 | let fresh record = 187 | let json = f () in 188 | let data, cached_data = cached_data req json in 189 | (match record with 190 | | Some record -> 191 | By_tag.set ~prev_tags:record.Record.tags req record; 192 | record.json <- Js.Nullable.return json; 193 | record.data <- Js.Nullable.return cached_data; 194 | record.tags <- List.map Tag.to_string req.tags 195 | | None -> 196 | let record = 197 | { 198 | Record.json = Js.Nullable.return json; 199 | data = Js.Nullable.return cached_data; 200 | tags = List.map Tag.to_string req.tags; 201 | } 202 | in 203 | By_tag.set ~prev_tags:[] req record; 204 | Js.Dict.set cache key record); 205 | data 206 | in 207 | match record with 208 | | None -> fresh None 209 | | Some record -> ( 210 | match Js.Nullable.toOption record.data with 211 | | None -> ( 212 | match Js.Nullable.toOption record.json with 213 | | None -> fresh (Some record) 214 | | Some json -> 215 | let data, cached_data = cached_data req json in 216 | record.data <- Js.Nullable.return cached_data; 217 | data) 218 | | Some (Cached_data (key, data)) -> ( 219 | match Witness.equal req.witness key with 220 | | None -> 221 | (* This shouldn't happen as requests are identified by 222 | path and we already found a corresponding cache record *) 223 | assert false 224 | | Some Eq -> data)) 225 | 226 | let invalidate req = 227 | let t = Lazy.force By_req.instance in 228 | By_req.invalidate t req 229 | 230 | let invalidate_tag = By_tag.invalidate 231 | end 232 | 233 | module Make (Route : sig 234 | type 'a t 235 | 236 | val http_method : 'a t -> [ `GET | `POST | `PUT | `DELETE ] 237 | val href : 'a t -> string 238 | val body : 'a t -> Ppx_deriving_json_runtime.t option 239 | val decode_response : 'a t -> Fetch.Response.t -> 'a Js.Promise.t 240 | val witness : 'a t -> 'a Witness.t 241 | end) = 242 | struct 243 | module F = Ppx_deriving_router_runtime.Make_fetch (Route) 244 | 245 | let run route = F.fetch route 246 | 247 | let to_req route tags = 248 | { 249 | witness = Route.witness route; 250 | path = Route.href route; 251 | input = 252 | lazy 253 | (Js.Json.stringify 254 | (match Route.body route with 255 | | None -> Js.Json.null 256 | | Some body -> body)); 257 | decode_response = Route.decode_response route; 258 | tags; 259 | } 260 | 261 | let fetch ?(tags = []) route = 262 | Cache.cached (to_req route tags) @@ fun () -> 263 | Promise.( 264 | let* response = F.fetch' route in 265 | match Fetch.Response.ok response with 266 | | true -> Fetch.Response.text response 267 | | false -> failwith "got non 200") 268 | 269 | let invalidate route = Cache.invalidate (to_req route []) 270 | let invalidate_tags tags = List.iter Cache.invalidate_tag tags 271 | end 272 | -------------------------------------------------------------------------------- /remote/browser/remote.mli: -------------------------------------------------------------------------------- 1 | (** Tag requests to invalidate cached responses. *) 2 | module Tag : sig 3 | type t 4 | 5 | val make : string -> t 6 | end 7 | 8 | (** Create a client for a given route declaration. *) 9 | module Make (Route : sig 10 | type 'a t 11 | 12 | val http_method : 'a t -> [ `DELETE | `GET | `POST | `PUT ] 13 | val href : 'a t -> string 14 | val body : 'a t -> Json.t option 15 | val decode_response : 'a t -> Fetch.response -> 'a Promise.t 16 | val witness : 'a t -> 'a Ppx_deriving_router_runtime.Witness.t 17 | end) : sig 18 | val fetch : ?tags:Tag.t list -> 'a Route.t -> 'a Promise.t 19 | (** send a request to the server, caching the response *) 20 | 21 | val run : 'a Route.t -> 'a Promise.t 22 | (** send a request to the server, WITHOUT caching the response *) 23 | 24 | val invalidate : 'a Route.t -> unit 25 | (** invalidate the cache for a given route *) 26 | 27 | val invalidate_tags : Tag.t list -> unit 28 | (** invalidate the cache for given tags *) 29 | end 30 | -------------------------------------------------------------------------------- /remote/native/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name remote.native) 3 | (name remote_native) 4 | (wrapped false) 5 | (modes native) 6 | (flags :standard -open Realm) 7 | (libraries 8 | containers 9 | hmap 10 | yojson 11 | htmlgen 12 | realm.native 13 | ppx_deriving_router.runtime_lib)) 14 | -------------------------------------------------------------------------------- /remote/native/remote.ml: -------------------------------------------------------------------------------- 1 | open ContainersLabels 2 | open Lwt.Infix 3 | module Witness = Ppx_deriving_router_runtime_lib.Witness 4 | 5 | type json = Json.t 6 | 7 | module Tag : sig 8 | type t 9 | 10 | val make : string -> t 11 | val to_string : t -> string 12 | end = struct 13 | type t = string 14 | 15 | let make x = x 16 | let to_string x = x 17 | end 18 | 19 | module Cache = struct 20 | module M = Map.Make (struct 21 | type t = string * string 22 | 23 | let compare = Ord.(pair string string) 24 | end) 25 | 26 | type record = Record : 'a Witness.t * 'a Promise.t -> record 27 | type t = record M.t 28 | 29 | let empty = M.empty 30 | let find = M.find_opt 31 | let add = M.add 32 | end 33 | 34 | module Context = struct 35 | type t = { 36 | mutable cache : Cache.t; 37 | mutable running : fetch list; 38 | mutable all_running : fetch list; 39 | mutable runtime_emitted : bool; 40 | } 41 | 42 | and fetch = 43 | | Fetch : { 44 | path : string; 45 | input : json; 46 | json_of_output : 'a -> json; 47 | promise : 'a Promise.t; 48 | tags : Tag.t list; 49 | } 50 | -> fetch 51 | 52 | and batch = fetch list 53 | 54 | let create () = 55 | { 56 | cache = Cache.empty; 57 | running = []; 58 | all_running = []; 59 | runtime_emitted = false; 60 | } 61 | 62 | let current : t Lwt.key = Lwt.new_key () 63 | 64 | let with_ctx ctx' f = 65 | let v = Lwt.with_value current (Some ctx') f in 66 | let running = ctx'.running in 67 | ctx'.running <- []; 68 | v, running 69 | 70 | let with_ctx_async ctx' f = 71 | let v = Lwt.with_value current (Some ctx') f in 72 | Lwt.map 73 | (fun v -> 74 | let running = ctx'.running in 75 | ctx'.running <- []; 76 | v, running) 77 | v 78 | 79 | let wait ctx = 80 | Lwt.join 81 | @@ List.filter_map 82 | ~f:(fun (Fetch { promise; _ }) -> 83 | match Lwt.state promise with 84 | | Fail _exn -> None 85 | | Return _v -> None 86 | | Sleep -> Some (promise >|= Fun.const ())) 87 | ctx.all_running 88 | 89 | module To_html = struct 90 | let runtime = 91 | Htmlgen.unsafe_raw 92 | {| 93 | 116 | |} 117 | 118 | let fetch_to_html' ~path ~input ~tags output = 119 | let tags = 120 | `List (List.map tags ~f:(fun tag -> `String (Tag.to_string tag))) 121 | in 122 | Htmlgen.unsafe_rawf 123 | "" 125 | (Htmlgen.single_quote_escape path) 126 | (Htmlgen.single_quote_escape (Yojson.Basic.to_string input)) 127 | (Htmlgen.single_quote_escape (Yojson.Basic.to_string tags)) 128 | (Htmlgen.single_quote_escape (Yojson.Basic.to_string output)) 129 | 130 | let fetch_to_html 131 | (Fetch { path; input; promise; json_of_output; tags }) = 132 | promise >|= fun output -> 133 | fetch_to_html' ~tags ~path ~input (json_of_output output) 134 | 135 | let batch_to_html ctx = function 136 | | [] -> Lwt.return Htmlgen.empty 137 | | batch -> 138 | Lwt_list.map_p fetch_to_html batch >|= fun htmls -> 139 | let htmls = 140 | if not ctx.runtime_emitted then ( 141 | ctx.runtime_emitted <- true; 142 | runtime :: htmls) 143 | else htmls 144 | in 145 | Htmlgen.splice htmls 146 | end 147 | 148 | let batch_to_html = To_html.batch_to_html 149 | end 150 | 151 | module Make (S : sig 152 | type 'a t 153 | 154 | val href : 'a t -> string 155 | val handle : 'a t -> 'a Promise.t 156 | val body : 'a t -> json option 157 | val encode_response : 'a t -> 'a -> json 158 | val witness : 'a t -> 'a Witness.t 159 | end) = 160 | struct 161 | type 'a route = 'a S.t 162 | 163 | let handle = S.handle 164 | let run : type a. a route -> a Lwt.t = handle 165 | 166 | let fetch : type a. ?tags:Tag.t list -> a route -> a Lwt.t = 167 | fun ?(tags = []) route -> 168 | let witness = S.witness route in 169 | let ctx = 170 | match Lwt.get Context.current with 171 | | None -> 172 | failwith 173 | "no Runner_ctx.t available, did you forgot to wrap the call \ 174 | site with Runner_ctx.with_ctx?" 175 | | Some ctx -> ctx 176 | in 177 | let path = S.href route in 178 | let input_json = Option.value (S.body route) ~default:`Null in 179 | let key = path, Yojson.Basic.to_string input_json in 180 | let fetch () = 181 | let promise = S.handle route in 182 | ctx.cache <- 183 | Cache.add key (Cache.Record (witness, promise)) ctx.cache; 184 | let running = 185 | Context.Fetch 186 | { 187 | path; 188 | input = input_json; 189 | json_of_output = S.encode_response route; 190 | promise; 191 | tags; 192 | } 193 | in 194 | ctx.running <- running :: ctx.running; 195 | ctx.all_running <- running :: ctx.all_running; 196 | promise 197 | in 198 | match Cache.find key ctx.cache with 199 | | Some (Record (witness', promise)) -> ( 200 | match Witness.equal witness' witness with 201 | | Some Eq -> promise 202 | | None -> assert false) 203 | | None -> fetch () 204 | end 205 | -------------------------------------------------------------------------------- /remote/native/remote.mli: -------------------------------------------------------------------------------- 1 | (** Tag requests to invalidate cached responses. *) 2 | module Tag : sig 3 | type t 4 | 5 | val make : string -> t 6 | end 7 | 8 | module Make (Route : sig 9 | type 'a t 10 | 11 | val href : 'a t -> string 12 | val body : 'a t -> Json.t option 13 | val encode_response : 'a t -> 'a -> Json.t 14 | val handle : 'a t -> 'a Promise.t 15 | val witness : 'a t -> 'a Ppx_deriving_router_runtime_lib.Witness.t 16 | end) : sig 17 | type 'a route = 'a Route.t 18 | 19 | val fetch : ?tags:Tag.t list -> 'a route -> 'a Promise.t 20 | (** [fetch route] runs [Route.handle route] but caches the results within the 21 | same [Runner.ctx]. *) 22 | 23 | val run : 'a route -> 'a Promise.t 24 | (** [run route] runs [Route.handle route]. *) 25 | 26 | val handle : 'a route -> 'a Promise.t 27 | (** [handle route] is equivalent to [Route.handle route] *) 28 | end 29 | 30 | module Context : sig 31 | type t 32 | 33 | val create : unit -> t 34 | (** Create a new context. *) 35 | 36 | val wait : t -> unit Lwt.t 37 | (** Wait for all running fetches finish. *) 38 | 39 | type batch 40 | (** A batch of requests. *) 41 | 42 | val batch_to_html : t -> batch -> Htmlgen.t Lwt.t 43 | (** [fetch_to_html fetch] converts a fetch request to an HTML representation. *) 44 | 45 | val with_ctx : t -> (unit -> 'a) -> 'a * batch 46 | (** [with_ctx ctx f] runs [f] in the context [ctx]. All fetch requests made 47 | during the execution of [f] are recorded and returned as a list. *) 48 | 49 | val with_ctx_async : 50 | t -> (unit -> 'a Promise.t) -> ('a * batch) Promise.t 51 | (** [with_ctx_async ctx f] is similar to [with_ctx] but [f] can be async. *) 52 | end 53 | --------------------------------------------------------------------------------