├── .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 |
16 | (React.string label) "OK"
17 |
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 |
137 |
141 | (React.string todo.text) label
142 |
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 | "Add"
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 |
"Remove completed todos"
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 |
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 |
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 |
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 ">";
127 | adds name;
128 | adds ">")
129 | | children ->
130 | adds ">";
131 | List.iter children ~f:(write buf);
132 | adds "";
133 | adds name;
134 | 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 |
--------------------------------------------------------------------------------