diff options
author | polwex <polwex@sortug.com> | 2025-06-15 04:59:49 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-06-15 04:59:49 +0700 |
commit | 71c20233ff79e696d0eeca2ce1462d3083fbcfed (patch) | |
tree | 4491f680fb6fe65e9d8606764c7000396856e93e | |
parent | 241dc9c99bed4dddbc748aad54cee5bf7d77ab92 (diff) |
and were done, just like that
27 files changed, 2957 insertions, 20 deletions
diff --git a/bs5/bs5.opam b/bs5/bs5.opam index 6347e75..fb09fe1 100644 --- a/bs5/bs5.opam +++ b/bs5/bs5.opam @@ -24,6 +24,7 @@ depends: [ "melange" "melange-json" "melange-json-native" + "melange-native" "ocamlformat" "melange-webapi" {with-dev-setup} "melange-fetch" {with-dev-setup} diff --git a/bs5/client/Hydrate.re b/bs5/client/Hydrate.re new file mode 100644 index 0000000..aadbca1 --- /dev/null +++ b/bs5/client/Hydrate.re @@ -0,0 +1,19 @@ +let%browser_only mockInitWebsocket: unit => unit = + () => [%mel.raw + {| + function mockInitWebsocket() { + console.log("Load JS"); + } +|} + ]; + +mockInitWebsocket(); + +let element = Webapi.Dom.Document.querySelector("#root", Webapi.Dom.document); + +switch (element) { +| Some(el) => + let _ = ReactDOM.Client.hydrateRoot(el, <App />); + (); +| None => Js.log("No root element found") +}; diff --git a/bs5/client/RouterRSC.re b/bs5/client/RouterRSC.re new file mode 100644 index 0000000..745c22d --- /dev/null +++ b/bs5/client/RouterRSC.re @@ -0,0 +1,85 @@ +module DOM = Webapi.Dom; +module Location = DOM.Location; +module History = DOM.History; +module ReadableStream = Webapi.ReadableStream; + +[@mel.scope "window"] [@mel.set] +external setNavigate: (Webapi.Dom.Window.t, string => unit) => unit = + "__navigate"; + +[@mel.module "react"] +external startTransition: (unit => unit) => unit = "startTransition"; +external readable_stream: ReadableStream.t = + "window.srr_stream.readable_stream"; + +let fetchApp = url => { + let headers = + Fetch.HeadersInit.make({"Accept": "application/react.component"}); + Fetch.fetchWithInit( + url, + Fetch.RequestInit.make(~method=Fetch.Get, ~headers, ()), + ); +}; + +module App = { + let initialData = + ReactServerDOMEsbuild.createFromReadableStream(readable_stream); + + [@react.component] + let make = () => { + let initialElement = React.Experimental.use(initialData); + let (data, setData) = React.Uncurried.useState(() => initialElement); + + let navigate = search => { + let location = DOM.window->DOM.Window.location; + let currentSearch = Location.search(location); + if (currentSearch == "?" ++ search) { + (); + } else { + let origin = Location.origin(location); + let pathname = Location.pathname(location); + let currentURL = origin ++ pathname; + let url = URL.makeExn(currentURL)->URL.setSearchAsString(search); + let app = fetchApp(URL.toString(url)); + let element = ReactServerDOMEsbuild.createFromFetch(app); + startTransition(() => { + setData(. _ => element); + History.pushState( + History.state(DOM.history), + "", + URL.toString(url), + DOM.history, + ); + }); + (); + }; + }; + + /* Publish navigate fn into window.__navigate */ + setNavigate(Webapi.Dom.window, navigate); + + <ReasonReactErrorBoundary + fallback={error => { + Js.log(error); + <h1> {React.string("Something went wrong")} </h1>; + }}> + data + </ReasonReactErrorBoundary>; + }; +}; + +let document: option(Webapi.Dom.Element.t) = [%mel.raw "window.document"]; + +let body = + Webapi.Dom.document + ->Webapi.Dom.Document.asHtmlDocument + ->Option.bind(Webapi.Dom.HtmlDocument.body); + +switch (document) { +| Some(element) => + startTransition(() => { + let _ = ReactDOM.Client.hydrateRoot(element, <App />); + (); + }) +| None => Js.log("Root element not found") +}; diff --git a/bs5/client/ServerOnlyRSC.re b/bs5/client/ServerOnlyRSC.re new file mode 100644 index 0000000..017c156 --- /dev/null +++ b/bs5/client/ServerOnlyRSC.re @@ -0,0 +1,15 @@ +let root = + Webapi.Dom.document + |> Webapi.Dom.Document.querySelector("#root") + |> Option.get; + +let root = ReactDOM.Client.createRoot(root); +let headers = + Fetch.HeadersInit.make({"Accept": "application/react.component"}); +let fetch = + Fetch.fetchWithInit( + Router.demoServerOnlyRSC, + Fetch.RequestInit.make(~method=Fetch.Get, ~headers, ()), + ); +let app = ReactServerDOMEsbuild.createFromFetch(fetch); +ReactDOM.Client.render(root, app); diff --git a/bs5/client/SinglePageRSC.re b/bs5/client/SinglePageRSC.re new file mode 100644 index 0000000..ee011f2 --- /dev/null +++ b/bs5/client/SinglePageRSC.re @@ -0,0 +1,40 @@ +module App = { + [@react.component] + let make = (~promise) => { + React.Experimental.use(promise); + }; +}; + +external readable_stream: Webapi.ReadableStream.t = + "window.srr_stream.readable_stream"; + +[@mel.module "react"] +external startTransition: (unit => unit) => unit = "startTransition"; + +try({ + let promise = + ReactServerDOMEsbuild.createFromReadableStream(readable_stream); + + let document: option(Webapi.Dom.Element.t) = [%mel.raw "window.document"]; + + switch (document) { + | Some(elem) => + startTransition(() => { + let app = <App promise />; + let _ = ReactDOM.Client.hydrateRoot(elem, app); + (); + }) + | None => Js.log("No root element found") + }; +}) { +| exn => + switch (Js.Exn.asJsExn(exn)) { + | Some(error) => + Js.log2("Error type:", Js.Exn.name(error)); + Js.log2("Stack:", Js.Exn.stack(error)); + Js.log2("Full error:", error); + | None => + Js.log("No JavaScript exception, got:"); + Js.log(exn); + } +}; diff --git a/bs5/client/dune b/bs5/client/dune new file mode 100644 index 0000000..669e755 --- /dev/null +++ b/bs5/client/dune @@ -0,0 +1,46 @@ +(env + (_ + (env-vars + ("DEMO_ENV" "development")))) + +(melange.emit + (enabled_if + (= %{profile} dev)) + (target app) + (module_systems + (es6 re.js)) + (libraries + melange + reason-react + melange.dom + melange-webapi + server-reason-react.url_js + melange-fetch + demo_shared_js) + (preprocess + (pps server-reason-react.browser_ppx -js reason-react-ppx melange.ppx))) + +(rule + (enabled_if + (= %{profile} dev)) + (alias client) + (deps + (package bs-5) + (alias_rec melange) + (:script build.mjs) + (:entrypoints + "app/demo/client/Hydrate.re.js" + "app/demo/client/SinglePageRSC.re.js" + "app/demo/client/RouterRSC.re.js" + "app/demo/client/ServerOnlyRSC.re.js") + (source_tree node_modules) + (file package.json) + (source_tree ../../packages/extract-client-components)) + (action + (run + node + %{script} + %{entrypoints} + --output=app/demo/client/ + --extract=true + --env=%{env:DEMO_ENV='production'}))) diff --git a/bs5/client/package-lock.json b/bs5/client/package-lock.json new file mode 100644 index 0000000..99b8b71 --- /dev/null +++ b/bs5/client/package-lock.json @@ -0,0 +1,1432 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "@pedrobslisboa/react-client": "^0.0.0-beta.2", + "esbuild": "^0.21.4", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-server-dom-webpack": "^19.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pedrobslisboa/react-client": { + "version": "0.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@pedrobslisboa/react-client/-/react-client-0.0.0-beta.2.tgz", + "integrity": "sha512-X/D2pASGDEsxozgEzdRWQPaw+O6F2sDh0OXy0tqBlLKx7+4Rbg2Mx/y3aNbkPpmnRD3tB942v3y/LEFmo6mPBw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "24.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", + "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-loose": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.1.tgz", + "integrity": "sha512-H68u/wiI8PAsSBclEIWwUg3dqEaDZXQHCovulbedgp78zJstjn7gDjfGgwUtW0BHi+KasryFLreHAGX/iXU85A==", + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001723", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz", + "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.167", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz", + "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==", + "license": "ISC", + "peer": true + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "peer": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT", + "peer": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT", + "peer": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC", + "peer": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-server-dom-webpack": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-server-dom-webpack/-/react-server-dom-webpack-19.1.0.tgz", + "integrity": "sha512-GUbawkNSN0oj8GnuNhMzsvyIHpXqqpAmyOY5NRqNNQ/M8wvUUN8YBoGjDUj9lbmBrmAHS65BByp6325CcWA0eg==", + "license": "MIT", + "dependencies": { + "acorn-loose": "^8.3.0", + "neo-async": "^2.6.1", + "webpack-sources": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0", + "webpack": "^5.59.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.42.0.tgz", + "integrity": "sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT", + "peer": true + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.2.tgz", + "integrity": "sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + } + } +} diff --git a/bs5/client/package.json b/bs5/client/package.json new file mode 100644 index 0000000..48b34d4 --- /dev/null +++ b/bs5/client/package.json @@ -0,0 +1,14 @@ +{ + "name": "client", + "version": "0.0.0", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@pedrobslisboa/react-client": "^0.0.0-beta.2", + "esbuild": "^0.21.4", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-server-dom-webpack": "^19.1.0" + } +} @@ -4,8 +4,7 @@ (= %{profile} "dev")) (deps server/server.exe - ; (alias_rec client) - ) + (alias_rec client)) (action (progn ; we want dune to write the file but not attach any fsevents listeners to it, @@ -21,16 +20,16 @@ (files ("./js/node_modules/@tailwindcss/cli/dist/index.mjs" as tailwind))) -; (rule -; (target output.css) -; (enabled_if -; (= %{profile} dev)) -; (alias client) -; (deps -; (source_tree ./) -; (source_tree ./../server) -; (:config tailwind.config.js) -; (:input styles.css)) -; (action -; (progn -; (run tailwind --config=%{config} --input=%{input} --output=%{target})))) +(rule + (target output.css) + (enabled_if + (= %{profile} dev)) + (alias client) + (deps + (source_tree ./) + (source_tree ./server) + (:config js/tailwind.config.js) + (:input js/styles.css)) + (action + (progn + (run tailwind --config=%{config} --input=%{input} --output=%{target})))) diff --git a/bs5/dune-project b/bs5/dune-project index 8445b07..5eceaf1 100644 --- a/bs5/dune-project +++ b/bs5/dune-project @@ -1,5 +1,13 @@ (lang dune 3.19) +(using melange 0.1) + +(using directory-targets 0.1) + +; (using mdx 0.4) +; (cram enable) +; + (name bs5) (generate_opam_files true) diff --git a/bs5/server/dune b/bs5/server/dune index 7a838ba..19ff1eb 100644 --- a/bs5/server/dune +++ b/bs5/server/dune @@ -31,8 +31,7 @@ (:standard -I +server-reason-react)) (preprocess (pps - lwt_ppx - ppx_yojson_conv server-reason-react.ppx - melange.ppx - melange-json-native.ppx))) + server-reason-react.melange_ppx + melange-json-native.ppx + lwt_ppx))) diff --git a/bs5/server/pages/Comments.re b/bs5/server/pages/Comments.re new file mode 100644 index 0000000..6af39ff --- /dev/null +++ b/bs5/server/pages/Comments.re @@ -0,0 +1,118 @@ +module Post = { + let make = () => { + <section> + <p> + {React.string( + "Notice how HTML for comments 'streams in' before the JavaScript (or React) has loaded on the page. In fact, the demo is entirely rendered in the server and doesn't use client-side JavaScript at all", + )} + </p> + <p> + {React.string("This demo is ")} + <b> {React.string("artificially slowed down")} </b> + {React.string(" while loading the comments data.")} + </p> + </section>; + }; +}; + +module Data = { + let delay = 4.0; + + let fakeData = [ + "Wait, it doesn't wait for React to load?", + "How does this even work?", + "I like marshmallows", + "!1!1!1! This is a comment", + "This is actually static from the server", + "But, imagine it's dynamic", + ]; + + let get = () => fakeData; + + let cached = ref(false); + let destroy = () => cached := false; + let promise = () => { + cached.contents + ? Lwt.return(fakeData) + : { + let%lwt () = Lwt_unix.sleep(delay); + cached.contents = true; + Lwt.return(fakeData); + }; + }; +}; + +module Comments = { + [@react.async.component] + let make = () => { + let comments = React.Experimental.use(Data.promise()); + + Lwt.return( + <div className="flex gap-4 flex-col"> + {comments + |> List.mapi((i, comment) => + <p + key={Int.to_string(i)} + className="font-semibold border-2 border-yellow-200 rounded-lg p-2 bg-yellow-600 text-slate-900"> + {React.string(comment)} + </p> + ) + |> React.list} + </div>, + ); + }; +}; + +module Page = { + [@react.component] + let make = () => { + <DemoLayout background=Theme.Color.Gray2> + <main + className={Theme.text(Theme.Color.Gray11)} + style={ReactDOM.Style.make(~display="flex", ~marginTop="16px", ())}> + <article className="flex gap-4 flex-col"> + <h1 + className={Cx.make([ + "text-4xl font-bold ", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string("Rendering React.Suspense on the server")} + </h1> + <Post /> + <section> + <h3 + className={Cx.make([ + "text-2xl font-bold mb-4", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string("Comments")} + </h3> + <React.Suspense fallback={<Spinner active=true />}> + <Comments /> + </React.Suspense> + </section> + <h2> {React.string("Thanks for reading!")} </h2> + </article> + </main> + </DemoLayout>; + }; +}; + +let handler = _request => { + Dream.stream( + ~headers=[("Content-Type", "text/html")], + response_stream => { + Data.destroy(); + + let pipe = data => { + let%lwt () = Dream.write(response_stream, data); + Dream.flush(response_stream); + }; + + let%lwt (stream, _abort) = + ReactDOM.renderToStream(<Document> <Page /> </Document>); + + Lwt_stream.iter_s(pipe, stream); + }, + ); +}; diff --git a/bs5/server/pages/Home.re b/bs5/server/pages/Home.re new file mode 100644 index 0000000..494a7b7 --- /dev/null +++ b/bs5/server/pages/Home.re @@ -0,0 +1,40 @@ +let handler = _request => { + let app = + <Document> + <div className={Cx.make(["py-16", "px-12"])}> + <div className="mb-8"> + <h1 + className={Cx.make([ + "font-extrabold text-5xl", + Theme.text(Theme.Color.Primary), + ])}> + {React.string("Demos for server-reason-react")} + </h1> + <div className="mt-8"> + <Text size=Medium> + "This is a list of links to all the demos for server-reason-react's features" + </Text> + <br /> + <Text size=Medium> + "If you want to learn more about server-reason-react, check out the " + </Text> + <Link.Text + target="_blank" + href="https://ml-in-barcelona.github.io/server-reason-react/local/server-reason-react/index.html"> + "documentation" + </Link.Text> + <Text size=Medium> " or " </Text> + <Link.Text + target="_blank" + href="https://github.com/ml-in-barcelona/server-reason-react"> + "repository" + </Link.Text> + <Text size=Medium> "." </Text> + </div> + </div> + <Router.Menu /> + </div> + </Document>; + + Dream.html(ReactDOM.renderToStaticMarkup(app)); +}; diff --git a/bs5/server/pages/Hydrate.re b/bs5/server/pages/Hydrate.re new file mode 100644 index 0000000..f887e34 --- /dev/null +++ b/bs5/server/pages/Hydrate.re @@ -0,0 +1,3 @@ +let doc = <Document script="/static/demo/Hydrate.re.js"> <App /> </Document>; +let toString = ReactDOM.renderToString(doc); +let toStatic = ReactDOM.renderToStaticMarkup(doc); diff --git a/bs5/server/pages/NoteItem.re b/bs5/server/pages/NoteItem.re new file mode 100644 index 0000000..f241cc1 --- /dev/null +++ b/bs5/server/pages/NoteItem.re @@ -0,0 +1,66 @@ +open Rsc; +open Lwt.Syntax; + +module NoteView = { + [@react.component] + let make = (~note: Note.t) => { + <div className="h-full"> + <div + className="flex flex-row items-center w-full mb-8 justify-between gap-4"> + <div className="flex flex-col items-left gap-4" role="menubar"> + <h1 + className={Cx.make([ + "text-4xl font-bold", + Theme.text(Theme.Color.Gray12), + ])}> + {React.string(note.title)} + </h1> + <Text size=Small role="status" color=Theme.Color.Gray10> + {"Last updated on " ++ Date.format_date(note.updated_at)} + </Text> + </div> + <Button noteId={Some(note.id)}> {React.string("Edit")} </Button> + <DeleteNoteButton noteId={note.id} /> + </div> + <NotePreview key="note-preview" body={Markdown.to_html(note.content)} /> + </div>; + }; +}; + +[@react.async.component] +let make = (~selectedId: option(int), ~isEditing: bool) => { + switch (selectedId) { + | None when isEditing => + Lwt.return( + <NoteEditor noteId=None initialTitle="Untitled" initialBody="" />, + ) + | None => + Lwt.return( + <div className="flex flex-col h-full items-center justify-center gap-2"> + <Text size=XXLarge> "🥺" </Text> + <Text> "Click a note on the left to view something!" </Text> + </div>, + ) + | Some(id) => + let+ note: result(Note.t, string) = DB.fetch_note(id); + + switch (note) { + | Ok(note) when !isEditing => <NoteView note /> + | Ok(note) => + <NoteEditor + noteId={Some(note.id)} + initialTitle={note.title} + initialBody={note.content} + /> + | Error(error) => + <div className="h-full w-full flex items-center justify-center"> + <div + className="h-full w-full flex flex-col items-center justify-center gap-4"> + <Text size=XXLarge> "❌" </Text> + <Text> "There's an error while loading a single note" </Text> + <Text weight=Bold> error </Text> + </div> + </div> + }; + }; +}; diff --git a/bs5/server/pages/NoteList.re b/bs5/server/pages/NoteList.re new file mode 100644 index 0000000..d284f0f --- /dev/null +++ b/bs5/server/pages/NoteList.re @@ -0,0 +1,52 @@ +open Lwt.Syntax; + +let is_substring = (a, b) => { + let len_a = String.length(a); + let len_b = String.length(b); + if (len_a > len_b) { + false; + } else { + let rec check = start => + if (start > len_b - len_a) { + false; + } else if (String.sub(b, start, len_a) == a) { + true; + } else { + check(start + 1); + }; + check(0); + }; +}; + +[@react.async.component] +let make = (~searchText: string) => { + let+ notes = DB.read_notes(); + + switch (notes) { + | Error(error) => + <div + className="mt-8 h-full w-full flex flex-col items-center justify-center gap-4"> + <Text size=XXLarge> "❌" </Text> + <Text> "Couldn't read notes file" </Text> + <Text weight=Bold> error </Text> + </div> + | Ok(notes) when notes->List.length == 0 => + <div className="mt-8"> + <Text> "There's no notes created yet!" </Text> + </div> + | Ok(notes) => + <ul className="mt-8"> + {notes + |> List.filter((note: Note.t) => + is_substring( + String.lowercase_ascii(searchText), + String.lowercase_ascii(note.title), + ) + ) + |> List.map((note: Note.t) => + <li key={Int.to_string(note.id)}> <SidebarNote note /> </li> + ) + |> React.list} + </ul> + }; +}; diff --git a/bs5/server/pages/RouterRSC.re b/bs5/server/pages/RouterRSC.re new file mode 100644 index 0000000..390a8db --- /dev/null +++ b/bs5/server/pages/RouterRSC.re @@ -0,0 +1,186 @@ +// let markdownStyles = (~background, ~text) => { +// Printf.sprintf( +// {| +// .markdown h1 { +// font-size: 2.25rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h2 { +// font-size: 1.875rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h3 { +// font-size: 1.5rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h4 { +// font-size: 1.25rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h5 { +// font-size: 1.125rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown h6 { +// font-size: 1rem; +// font-weight: bold; +// line-height: 2.5; +// } + +// .markdown p { +// font-size: 1rem; +// margin-bottom: 1rem; +// } + +// .markdown ul, .markdown ol { +// padding-left: 2rem; +// margin-bottom: 1rem; +// } + +// .markdown li { +// margin-bottom: 0.5rem; +// } + +// .markdown blockquote { +// border-left: 4px solid %s; +// padding-left: 1rem; +// margin: 1.5rem 0; +// font-style: italic; +// } + +// .markdown pre { +// padding: 1rem; +// margin: 1.5rem 0; +// background-color: %s; +// color: %s; +// border-radius: 0.375rem; +// } + +// .markdown code { +// display: block; +// margin: 1rem; +// padding-left: 1rem; +// padding-right: 1rem; +// font-family: monospace; +// background-color: %s; +// color: %s; +// padding: 0.25rem 0.5rem; +// border-radius: 0.25rem; +// } +// |}, +// background, +// background, +// text, +// background, +// text, +// ); +// }; + +module App = { + [@react.async.component] + let make = (~selectedId, ~isEditing, ~searchText) => { + Lwt.return( + <html> + <head> + + <meta charSet="utf-8" /> + <link rel="stylesheet" href="/output.css" /> + </head> + // <style + // dangerouslySetInnerHTML={ + // "__html": + // markdownStyles( + // ~background=Theme.Color.gray2, + // ~text=Theme.Color.gray12, + // ), + // } + // /> + <body> + <div id="root"> + <DemoLayout background=Theme.Color.Gray2 mode=FullScreen> + <div className="flex flex-row gap-8"> + <section + className="flex-1 basis-1/4 gap-4 min-w-[400px]" + key="sidebar"> + <section + className="flex flex-col gap-1 z-1 max-w-[85%] pointer-events-none mb-6" + key="sidebar-header"> + <Text size=Large weight=Bold> + "server-reason-react notes" + </Text> + <p> + <Text color=Theme.Color.Gray10> "migrated from " </Text> + <Link.Text + size=Text.Small + href="https://github.com/reactjs/server-components-demo"> + "reactjs/server-components-demo" + </Link.Text> + <Text color=Theme.Color.Gray10> + " with (server)-reason-react and Melange" + </Text> + </p> + </section> + <section + className="mt-4 mb-4 flex flex-row gap-2" + role="menubar" + key="menubar"> + <SearchField searchText selectedId isEditing /> + </section> + <nav className="mt-4"> + <div className="mb-4"> <Hr /> </div> + <div className="mb-4"> + <Button noteId=None> + {React.string("Create a note")} + </Button> + </div> + <Hr /> + <React.Suspense fallback={<NoteListSkeleton />}> + <NoteList searchText /> + </React.Suspense> + </nav> + </section> + <section + key="note-viewer" className="flex-1 basis-3/4 max-w-[75%]"> + <React.Suspense fallback={<NoteSkeleton isEditing />}> + <NoteItem selectedId isEditing /> + </React.Suspense> + </section> + </div> + </DemoLayout> + </div> + </body> + </html>, + ); + }; +}; + +let handler = request => { + let selectedId = + Dream.query(request, "selectedId") + |> Option.map(string => int_of_string_opt(string)) + |> Option.value(~default=None); + + let isEditing = + Dream.query(request, "isEditing") + |> Option.map(v => v == "true") + |> Option.value(~default=false); + + let searchText = + Dream.query(request, "searchText") |> Option.value(~default=""); + + Rsc.DreamRSC.create_from_request( + ~bootstrap_modules=["/static/demo/RouterRSC.re.js"], + <App selectedId isEditing searchText />, + request, + ); +}; diff --git a/bs5/server/pages/ServerOnlyRSC.re b/bs5/server/pages/ServerOnlyRSC.re new file mode 100644 index 0000000..8e166aa --- /dev/null +++ b/bs5/server/pages/ServerOnlyRSC.re @@ -0,0 +1,46 @@ +let handler = request => { + let isRSCheader = + Dream.header(request, "Accept") == Some("application/react.component"); + + let app = + <DemoLayout background=Theme.Color.Gray2> + <div className="flex flex-col items-center justify-center h-full gap-4"> + <span className="text-gray-400 text-center"> + {React.string( + "The client will fetch the server component from the server and run createFromFetch", + )} + <br /> + {React.string("asking for the current time (in seconds) since")} + <br /> + {React.string("00:00:00 GMT, Jan. 1, 1970")} + </span> + <h1 + className={Cx.make([ + "font-bold text-4xl", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string(string_of_float(Unix.gettimeofday()))} + </h1> + </div> + </DemoLayout>; + + if (isRSCheader) { + Dream.stream(response_stream => { + let%lwt () = + ReactServerDOM.render_model( + ~debug=true, + ~subscribe=data => Dream.write(response_stream, data), + app, + ); + Lwt.return(); + }); + } else { + Dream.html( + ReactDOM.renderToString( + <Document script="/static/demo/ServerOnlyRSC.re.js"> + React.null + </Document>, + ), + ); + }; +}; diff --git a/bs5/server/pages/SidebarNote.re b/bs5/server/pages/SidebarNote.re new file mode 100644 index 0000000..334ca74 --- /dev/null +++ b/bs5/server/pages/SidebarNote.re @@ -0,0 +1,32 @@ +open Rsc; +[@react.component] +let make = (~note: Note.t) => { + let lastUpdatedAt = + if (Date.is_today(note.updated_at)) { + Date.format_time(note.updated_at); + } else { + Date.format_date(note.updated_at); + }; + + let summary = + note.content |> Markdown.extract_text |> Markdown.summarize(~words=20); + + <SidebarNoteContent + id={note.id} + title={note.title} + expandedChildren={ + <div className="mt-2"> + {switch (String.trim(summary)) { + | "" => <i> {React.string("(No content)")} </i> + | s => <Text size=Small color=Theme.Color.Gray11> s </Text> + }} + </div> + }> + <header + className={Cx.make(["max-w-[85%] flex flex-col gap-2"])} + style={ReactDOM.Style.make(~zIndex="1", ())}> + <Text size=Large weight=Bold> {note.title} </Text> + <Text size=Small> lastUpdatedAt </Text> + </header> + </SidebarNoteContent>; +}; diff --git a/bs5/server/pages/SinglePageRSC.re b/bs5/server/pages/SinglePageRSC.re new file mode 100644 index 0000000..685069a --- /dev/null +++ b/bs5/server/pages/SinglePageRSC.re @@ -0,0 +1,232 @@ +open Rsc; +module Section = { + [@react.component] + let make = (~title, ~children, ~description=?) => { + <Stack gap=2 justify=`start> + <h2 + className={Cx.make([ + "text-3xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string(title)} + </h2> + {switch (description) { + | Some(description) => + <Text color=Theme.Color.Gray10> description </Text> + | None => React.null + }} + <div className="mb-4" /> + children + </Stack>; + }; +}; + +module ExpandedContent = { + [@react.component] + let make = (~id, ~content: string, ~updatedAt: float, ~title: string) => { + let lastUpdatedAt = + if (Date.is_today(updatedAt)) { + Date.format_time(updatedAt); + } else { + Date.format_date(updatedAt); + }; + + let summary = + content |> Markdown.extract_text |> Markdown.summarize(~words=20); + + <Expander + id + title + expandedChildren={ + <div className="mt-2"> + {switch (String.trim(summary)) { + | "" => <i> {React.string("(No content)")} </i> + | s => <Text size=Small color=Theme.Color.Gray11> s </Text> + }} + <Counter.Double initial=22 /> + </div> + }> + <header + className={Cx.make(["max-w-[85%] flex flex-col gap-2"])} + style={ReactDOM.Style.make(~zIndex="1", ())}> + <Text size=Large weight=Bold> title </Text> + <Text size=Small> lastUpdatedAt </Text> + </header> + </Expander>; + }; +}; + +module Page = { + [@react.async.component] + let make = () => { + let promiseIn2 = + Lwt.bind(Lwt_unix.sleep(2.0), _ => + Lwt.return("Solusionao in 2 seconds!") + ); + let promiseIn4 = + Lwt.bind(Lwt_unix.sleep(4.0), _ => + Lwt.return("Solusionao in 4 seconds!") + ); + + Lwt.return( + <Stack gap=8 justify=`start> + <Stack gap=2 justify=`start> + <h1 + className={Cx.make([ + "text-3xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string( + "Server side rendering server components and client components", + )} + </h1> + <Text color=Theme.Color.Gray10> + "React server components. Lazy loading of client components. Client props encodings, such as promises, React elements, and primitive types." + </Text> + </Stack> + <Hr /> + <Section + title="Counter" + description="Passing int into a client component, the counter starts at 45 and counts by one"> + <Counter initial=45 /> + </Section> + <Hr /> + <Section + title="Debug client props" + description="Passing client props into a client component"> + <Debug_props + string="Title" + float=1.1 + bool_true=true + bool_false=false + header={Some(<div> {React.string("H E A D E R")} </div>)} + string_list=["Item 1", "Item 2"] + promise=promiseIn2> + <div> + {React.string( + "This footer is a React.element as a server component into client prop, yay!", + )} + </div> + </Debug_props> + </Section> + <Hr /> + <Section + title="Debug client props" + description="Passing client props into a client component"> + <Debug_props + string="Title" + int=99 + float=1.1 + bool_true=true + bool_false=false + header={Some(<div> {React.string("H E A D E R")} </div>)} + string_list=["Item 1", "Item 2"] + promise=promiseIn2> + <div> + {React.string( + "This footer is a React.element as a server component into client prop, yay!", + )} + </div> + </Debug_props> + </Section> + <Hr /> + <Section + title="Pass another promise prop" + description="Sending a promise from the server to the client"> + <Promise_renderer promise=promiseIn4 /> + </Section> + <Hr /> + <Section + title="Pass a client component prop" + description="Sending a client component from the server to the client (that contains another client component)"> + <ExpandedContent + id=1 + title="Titulaso" + updatedAt=1653561600.0 + content="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + /> + </Section> + <Hr /> + <h1 + className={Cx.make([ + "text-5xl", + "font-bold", + Theme.text(Theme.Color.Gray11), + ])}> + {React.string("Server functions")} + </h1> + <Hr /> + <Section + title="Server function from props on a Client Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionFromPropsClient + actionOnClick=ServerFunctions.simpleResponse + /> + </Section> + <Hr /> + <Section + title="Server function with simple response" + description="Server function imported and called directly on a client component"> + <ServerActionWithSimpleResponse /> + </Section> + <Hr /> + <Section + title="Server function with error" + description="Server function with error"> + <ServerActionWithError /> + </Section> + <Hr /> + <Section + title="Server function with FormData" + description="Server function with FormData"> + <ServerActionWithFormData /> + </Section> + <Hr /> + <Section + title="Server function with FormData on action attribute on Server Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionWithFormDataServer /> + </Section> + <Hr /> + <Section + title="Server function with FormData on formAction attribute on Server Component" + description="In this case, react will use the server function from the window.__server_functions_manifest_map"> + <ServerActionWithFormDataFormAction /> + </Section> + <Hr /> + <Section + title="Server function with FormData with extra arg" + description="It shows that it's possible to pass extra arguments to the server function on forms"> + <ServerActionWithFormDataWithArg /> + </Section> + <Hr /> + </Stack>, + ); + }; +}; + +module App = { + [@react.component] + let make = () => { + <html> + <head> + <meta charSet="utf-8" /> + <link rel="stylesheet" href="/output.css" /> + </head> + <body> + <div id="root"> + <DemoLayout background=Theme.Color.Gray2> <Page /> </DemoLayout> + </div> + </body> + </html>; + }; +}; + +let handler = request => + DreamRSC.create_from_request( + ~bootstrap_modules=["/static/demo/SinglePageRSC.re.js"], + <App />, + request, + ); diff --git a/bs5/server/server.ml b/bs5/server/server.ml index 2047624..c7c18e9 100644 --- a/bs5/server/server.ml +++ b/bs5/server/server.ml @@ -1,8 +1,30 @@ (* TODO *) (* Dream.origin_referrer_check *) + +let getAndPost path handler = + Dream.scope "/" [] + [ + Dream.get path handler; + Dream.post path Rsc.DreamRSC.stream_function_response; + ] + let router = Dream.router ([ + (* rendering tricks *) + Dream.get "/output.css" + (Dream.from_filesystem "./_build/default/demo" "output.css"); + Dream.get "/static/**" (Dream.static "./_build/default/client/app"); + getAndPost Router.demoRenderToString (fun _ -> + Dream.html Pages.Hydrate.toString); + getAndPost Router.demoRenderToString (fun _ -> + Dream.html Pages.Hydrate.toStatic); + (* more demos *) + getAndPost Router.demoRenderToStream Pages.Comments.handler; + getAndPost Router.demoSinglePageRSC Pages.SinglePageRSC.handler; + getAndPost Router.demoRouterRSC Pages.RouterRSC.handler; + getAndPost Router.demoServerOnlyRSC Pages.ServerOnlyRSC.handler; + (* my actual routes *) Dream.get "/root" (fun _ -> Dream.html "Roooot"); Dream.get "/login" (fun _ -> Dream.html "Welcome"); Dream.get "/blog/:poast" (fun req -> @@ -21,4 +43,4 @@ let server = @@ Middleware.Promise.count_requests @@ Middleware.Counter.count_requests router -let () = Dream.run server +let () = Dream.run ~port:4444 server diff --git a/bs5/universal/js/ClientRouter.re b/bs5/universal/js/ClientRouter.re new file mode 100644 index 0000000..40083c2 --- /dev/null +++ b/bs5/universal/js/ClientRouter.re @@ -0,0 +1,22 @@ +type t = Router.t(Fetch.Response.t); + +external navigate: string => unit = "window.__navigate"; +external useAction: + (string, string) => ((Router.payload, Router.location, unit) => unit, bool) = + "window.__useAction"; + +let useRouter: unit => t = + () => { + { + location: Router.initialLocation, + navigate: str => { + navigate(Router.locationToString(str)); + }, + useAction: (endpoint, method) => { + useAction(endpoint, method); + }, + refresh: str => { + Js.log(str); + }, + }; + }; diff --git a/bs5/universal/js/Dream.re b/bs5/universal/js/Dream.re new file mode 100644 index 0000000..19f9187 --- /dev/null +++ b/bs5/universal/js/Dream.re @@ -0,0 +1 @@ +let log = Js.log2; diff --git a/bs5/universal/js/ReactServerDOMEsbuild.js b/bs5/universal/js/ReactServerDOMEsbuild.js new file mode 100644 index 0000000..7961739 --- /dev/null +++ b/bs5/universal/js/ReactServerDOMEsbuild.js @@ -0,0 +1,282 @@ +/* + * This file is a bundler integration between react (react-client/flight), esbuild and server-reason-react. + * + * Similar resources + * **react-server-dom-webpack** + * - https://github.com/facebook/react/blob/5c56b873efb300b4d1afc4ba6f16acf17e4e5800/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js#L156-L194 + * - https://github.com/facebook/react/blob/main/packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js + * + * Take a look at new’s react-server-dom-parcel https://github.com/facebook/react/pull/31725 + * + * What’s possible with esbuild + * + * - RSC server + client https://github.com/jacob-ebey/oneup/blob/main/packages/cli/index.ts + * - https://github.com/jfortunato/esbuild-plugin-manifest/blob/master/src/index.ts +*/ + +import ReactClientFlight from "@pedrobslisboa/react-client/flight"; + +const is_debug = false; + +const debug = (...args) => { + if (is_debug && process.env.NODE_ENV === "development") { + console.log(...args); + } +}; + +const ReactFlightClientStreamConfigWeb = { + createStringDecoder() { + return new TextDecoder(); + }, + + readPartialStringChunk(decoder, buffer) { + return decoder.decode(buffer, { stream: true }); + }, + + readFinalStringChunk(decoder, buffer) { + return decoder.decode(buffer); + }, +}; + +const badgeFormat = "%c%s%c "; + +// Same badge styling as DevTools. +const badgeStyle = + // We use a fixed background if light-dark is not supported, otherwise + // we use a transparent background. + "background: #e6e6e6;" + + "background: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.25));" + + "color: #000000;" + + "color: light-dark(#000000, #ffffff);" + + "border-radius: 2px"; + +const resetStyle = ""; +const pad = " "; + +const bind = Function.prototype.bind; + +const ReactClientConsoleConfigBrowser = { + bindToConsole(methodName, args, badgeName) { + let offset = 0; + switch (methodName) { + case "dir": + case "dirxml": + case "groupEnd": + case "table": { + // These methods cannot be colorized because they don't take a formatting string. + return bind.apply(console[methodName], [console].concat(args)); + } + case "assert": { + // assert takes formatting options as the second argument. + offset = 1; + } + } + + const newArgs = args.slice(0); + if (typeof newArgs[offset] === "string") { + newArgs.splice( + offset, + 1, + badgeFormat + newArgs[offset], + badgeStyle, + pad + badgeName + pad, + resetStyle + ); + } else { + newArgs.splice( + offset, + 0, + badgeFormat, + badgeStyle, + pad + badgeName + pad, + resetStyle + ); + } + + // The "this" binding in the "bind"; + newArgs.unshift(console); + + return bind.apply(console[methodName], newArgs); + }, +}; + +const ID = 0; +const NAME = 1; +const BUNDLES = 2; + +const ReactFlightClientConfigBundlerEsbuild = { + prepareDestinationForModule(moduleLoading, nonce, metadata) { + debug("prepareDestinationForModule", moduleLoading, nonce, metadata); + return; + }, + + resolveClientReference(bundlerConfig, metadata) { + debug("resolveClientReference", bundlerConfig, metadata); + // Reference is already resolved during the build + return { + type: "ClientComponent", + id: metadata[ID], + name: metadata[NAME], + bundles: metadata[BUNDLES], + }; + }, + + resolveServerReference(bundlerConfig, ref) { + debug("resolveServerReference", bundlerConfig, ref); + + return { + type: "ServerFunction", + id: ref, + }; + }, + + preloadModule(metadata) { + debug("preloadModule", metadata); + /* TODO: Does it make sense to preload a module in esbuild? */ + return undefined; + }, + + requireModule(metadata) { + const getModule = (type, id) => { + switch (type) { + case "ServerFunction": + const fn = window.__server_functions_manifest_map[id]; + + return fn; + case "ClientComponent": + const component = window.__client_manifest_map[id]; + + return component + } + } + + const module = getModule(metadata.type, metadata.id); + if (!module) { + throw new Error(`Could not find module of type ${metadata.type} with id: ${metadata.id}`); + } + + return module + }, +}; + +/* TODO: Can we use the real thing, instead of mocks/vendored code here? */ +const ReactServerDOMEsbuildConfig = { + ...ReactFlightClientStreamConfigWeb, + ...ReactClientConsoleConfigBrowser, + ...ReactFlightClientConfigBundlerEsbuild, + rendererVersion: "19.0.0", + rendererPackageName: "react-server-dom-esbuild", + usedWithSSR: true, +}; + +const { + createResponse, + createServerReference: createServerReferenceImpl, + processReply, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} = ReactClientFlight(ReactServerDOMEsbuildConfig); + +function startReadingFromStream(response, stream) { + const reader = stream.getReader(); + function progress({ done, value }) { + if (done) { + close(response); + return; + } + const buffer = value; + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function callCurrentServerCallback(callServer) { + return function (id, args) { + if (!callServer) { + throw new Error( + "No server callback has been registered. Call setServerCallback to register one." + ); + } + return callServer(id, args); + }; +} + +export function createFromReadableStream(stream, options) { + const response = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +function createResponseFromOptions(options) { + let response = createResponse( + // [QUESTION] Should we have for client components the same as we have for server functions? + null, // bundlerConfig + // serverFunctionsConfig, this is the manifest that can contain configs related to server functions + // Unfortunatelly, react requires it to not be null, to run resolveServerReference + {}, + null, // moduleLoading + callCurrentServerCallback(options ? options.callServer : undefined), + undefined, // encodeFormAction + undefined, // nonce + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + undefined, // TODO: findSourceMapUrl + false /* __DEV__ ? (options ? options.replayConsoleLogs !== false : true) */, + undefined /* __DEV__ && options && options.environmentName + ? options.environmentName + : undefined */ + ); + + return response; +} + +export function createFromFetch(promise, options) { + const response = createResponseFromOptions(options); + promise.then( + function (r) { + startReadingFromStream(response, r.body); + }, + function (e) { + reportGlobalError(response, e); + } + ); + return getRoot(response); +} + +export const createServerReference = createServerReferenceImpl; + +export const encodeReply = ( + value, + options = { temporaryReferences: undefined, signal: undefined } +) => { + return new Promise((resolve, reject) => { + const abort = processReply( + value, + "", + options && options.temporaryReferences + ? options.temporaryReferences + : undefined, + resolve, + reject + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(signal.reason); + } else { + const listener = () => { + abort(signal.reason); + signal.removeEventListener("abort", listener); + }; + signal.addEventListener("abort", listener); + } + } + }); +}; diff --git a/bs5/universal/js/ReactServerDOMEsbuild.re b/bs5/universal/js/ReactServerDOMEsbuild.re new file mode 100644 index 0000000..957d54b --- /dev/null +++ b/bs5/universal/js/ReactServerDOMEsbuild.re @@ -0,0 +1,72 @@ +type callServer('arg, 'result) = + (string, list('arg)) => Js.Promise.t('result); + +type options('arg, 'result) = {callServer: callServer('arg, 'result)}; + +[@mel.module "./ReactServerDOMEsbuild.js"] +external createFromReadableStreamImpl: + (Webapi.ReadableStream.t, ~options: options('arg, 'result)=?, unit) => + Js.Promise.t('result) = + "createFromReadableStream"; + +[@mel.module "./ReactServerDOMEsbuild.js"] +external createFromFetchImpl: + (Js.Promise.t(Fetch.response), ~options: options('arg, 'result)=?, unit) => + React.element = + "createFromFetch"; + +[@mel.module "./ReactServerDOMEsbuild.js"] +external createServerReferenceImpl: + ( + string, // ServerReferenceId + callServer('arg, 'result), + // EncodeFormActionCallback (optional) (We're not using this right now) + option('encodeFormActionCallback), + // FindSourceMapURLCallback (optional, DEV-only) (We're not using this right now) + option('findSourceMapURLCallback), + // functionName (optional) + option(string) + ) => + // actionCallback is a function that takes N arguments and returns a promise + // As we don't have control over the number of arguments, we need to pass it as 'actionCallback + 'action = + "createServerReference"; + +[@mel.module "./ReactServerDOMEsbuild.js"] +external encodeReply: list('arg) => Js.Promise.t(string) = "encodeReply"; + +let callServer = (path: string, args) => { + let headers = + Fetch.HeadersInit.make({ + "Accept": "application/react.action", + "ACTION_ID": path, + }); + encodeReply(args) + |> Js.Promise.then_(body => { + let body = Fetch.BodyInit.make(body); + Fetch.fetchWithInit( + "/", + Fetch.RequestInit.make(~method=Fetch.Post, ~headers, ~body, ()), + ) + |> Js.Promise.then_(result => { + let body = Fetch.Response.body(result); + createFromReadableStreamImpl(body, ()); + }); + }); +}; + +let createFromReadableStream = stream => { + createFromReadableStreamImpl( + stream, + ~options={callServer: callServer}, + (), + ); +}; + +let createFromFetch = promise => { + createFromFetchImpl(promise, ~options={callServer: callServer}, ()); +}; + +let createServerReference = serverReferenceId => { + createServerReferenceImpl(serverReferenceId, callServer, None, None, None); +}; diff --git a/bs5/universal/js/ReactServerDOMWebpack.re b/bs5/universal/js/ReactServerDOMWebpack.re new file mode 100644 index 0000000..a2067cd --- /dev/null +++ b/bs5/universal/js/ReactServerDOMWebpack.re @@ -0,0 +1,78 @@ +type callServer('arg, 'result) = + (string, list('arg)) => Js.Promise.t('result); + +type options('arg, 'result) = {callServer: callServer('arg, 'result)}; + +[@mel.module "react-server-dom-webpack/client"] +external createFromReadableStreamImpl: + (Webapi.ReadableStream.t, ~options: options('arg, 'result)=?, unit) => + Js.Promise.t('result) = + "createFromReadableStream"; + +[@mel.module "react-server-dom-webpack/client"] +external createFromFetchImpl: + (Js.Promise.t(Fetch.response), ~options: options('arg, 'result)=?, unit) => + React.element = + "createFromFetch"; + +[@mel.module "react-server-dom-webpack/client"] +external createServerReferenceImpl: + ( + string, // ServerReferenceId + callServer('arg, 'result), + // EncodeFormActionCallback (optional) (We're not using this right now) + option('encodeFormActionCallback), + // FindSourceMapURLCallback (optional, DEV-only) (We're not using this right now) + option('findSourceMapURLCallback), + // functionName (optional) + option(string) + ) => + // actionCallback is a function that takes N arguments and returns a promise + // As we don't have control over the number of arguments, we need to pass it as 'actionCallback + 'action = + "createServerReference"; + +[@mel.module "react-server-dom-webpack/client"] +external encodeReply: list('arg) => Js.Promise.t(string) = "encodeReply"; + +let callServer = (path: string, args) => { + let headers = + Fetch.HeadersInit.make({ + "Accept": "application/react.action", + "ACTION_ID": path, + }); + encodeReply(args) + |> Js.Promise.then_(body => { + let body = Fetch.BodyInit.make(body); + Fetch.fetchWithInit( + "/", + Fetch.RequestInit.make(~method=Fetch.Post, ~headers, ~body, ()), + ) + |> Js.Promise.then_(result => { + let body = Fetch.Response.body(result); + createFromReadableStreamImpl(body, ()); + }); + }); +}; + +let createFromReadableStream = stream => { + createFromReadableStreamImpl( + stream, + ~options={callServer: callServer}, + (), + ); +}; + +let createFromFetch = promise => { + createFromFetchImpl(promise, ~options={callServer: callServer}, ()); +}; + +let createServerReference = (serverReferenceId, functionName) => { + createServerReferenceImpl( + serverReferenceId, + callServer, + None, + None, + functionName, + ); +}; diff --git a/bs5/universal/js/dune b/bs5/universal/js/dune new file mode 100644 index 0000000..20b7dd2 --- /dev/null +++ b/bs5/universal/js/dune @@ -0,0 +1,27 @@ +(library + (name demo_shared_js) + (modes melange) + (wrapped false) + (libraries + reason-react + melange-webapi + melange.belt + melange.js + melange-fetch + melange.dom + server-reason-react.url_js + melange-json) + (melange.runtime_deps ReactServerDOMEsbuild.js) + (preprocess + (pps + server-reason-react.browser_ppx + -js + server-reason-react.ppx + -melange + melange.ppx + reason-react-ppx + melange-json.ppx))) + +(copy_files + (mode fallback) + (files "../native/shared/*.re")) |