diff --git a/Makefile b/Makefile index 892275de4..e6012e749 100644 --- a/Makefile +++ b/Makefile @@ -132,11 +132,11 @@ build-bench: ## Run benchmark .PHONY: bench bench: build-bench ## Run benchmark - @$(DUNE) exec bench/main.exe --profile=release --display-separate-messages --no-print-directory + @$(DUNE) exec benchmark/main.exe --profile=release --display-separate-messages --no-print-directory .PHONY: bench-watch bench-watch: build-bench ## Run benchmark in watch mode - @$(DUNE) exec bench/main.exe --profile=release --display-separate-messages --no-print-directory --watch + @$(DUNE) exec benchmark/main.exe --profile=release --display-separate-messages --no-print-directory --watch .PHONY: once once: ## Run benchmark once diff --git a/arch/server/package.json b/arch/server/package.json index f111f7a82..e79abe50d 100644 --- a/arch/server/package.json +++ b/arch/server/package.json @@ -2,7 +2,7 @@ "name": "app", "version": "0.0.1", "scripts": { - "react-dom-server": "node react-dom-server.js", + "react-dom-server": "bun react-dom-server.js", "render-html-to-stream": "bun render-html-to-stream.js", "render-rsc-to-stream": "bun --conditions react-server render-rsc-to-stream.js", "react-server-dom-webpack": "node --conditions react-server react-server-dom-webpack.js" diff --git a/arch/server/react-dom-server.js b/arch/server/react-dom-server.js index cd7d733da..4cb5e5a60 100644 --- a/arch/server/react-dom-server.js +++ b/arch/server/react-dom-server.js @@ -2,60 +2,60 @@ const React = require("react"); const ReactDOM = require("react-dom/server"); let first = React.createElement( - "div", - { key: "fi", value1: "aaa", style: { margin: "1px" } }, - ["first"] + "div", + { key: "fi", value1: "aaa", style: { margin: "1px" } }, + ["first"], ); let clon = React.cloneElement( - first, - { value: "bbb", more: 22, style: { margin: "1", padding: "0px" } }, - ["asdf"] + first, + { value: "bbb", more: 22, style: { margin: "1", padding: "0px" } }, + ["asdf"], ); const { Provider, Consumer } = React.createContext(10); var app = () => { - /* https://fb.me/react-uselayouteffect-ssr */ - React.useLayoutEffect(() => { - console.log("asdfdsf"); + /* https://fb.me/react-uselayouteffect-ssr */ + React.useLayoutEffect(() => { + console.log("asdfdsf"); - return () => { - console.log("asdfsdf"); - }; - }); - return React.createElement("div", { className: "contenido" }, []); + return () => { + console.log("asdfsdf"); + }; + }); + return React.createElement("div", { className: "contenido" }, []); }; var app = () => { - let [state, setState] = React.useState(0); + let [state, setState] = React.useState(0); - React.useEffect(() => { - setState(state + 1); - console.log("asdfdsf"); + React.useEffect(() => { + setState(state + 1); + console.log("asdfdsf"); - return () => { - console.log("asdfsdf"); - }; - }); - return React.createElement("div", { className: "contenido" }, []); + return () => { + console.log("asdfsdf"); + }; + }); + return React.createElement("div", { className: "contenido" }, []); }; var app = () => { - let [state, setState] = React.useState(0); - let ref = React.useRef(true); - console.log(state); - if (ref.current) { - setState(state + 1); - ref.current = false; - } - React.useEffect(() => { - console.log("asfsdafsafsadf"); - }); - React.useEffect(() => { - console.log("asfsdafsafsadf"); - }, [state]); - console.log(state); - return React.createElement("div", null, [state]); + let [state, setState] = React.useState(0); + let ref = React.useRef(true); + console.log(state); + if (ref.current) { + setState(state + 1); + ref.current = false; + } + React.useEffect(() => { + console.log("asfsdafsafsadf"); + }); + React.useEffect(() => { + console.log("asfsdafsafsadf"); + }, [state]); + console.log(state); + return React.createElement("div", null, [state]); }; /* var app = () => { @@ -67,35 +67,44 @@ var app = () => { var ctx = React.createContext(10); var context_user = () => { - let a = React.useContext(ctx); - console.log(a); - return React.createElement("div", { key: 1 }, [a]); + let a = React.useContext(ctx); + console.log(a); + return React.createElement("div", { key: 1 }, [a]); }; var app = () => { - let ref = React.useRef(333); - console.log(ref); - return React.createElement( - ctx.Provider, - { value: 0, ref: ref }, - React.createElement(context_user) - ); + let ref = React.useRef(333); + console.log(ref); + return React.createElement( + ctx.Provider, + { value: 0, ref: ref }, + React.createElement(context_user), + ); }; var app = React.forwardRef(() => { - let ref = React.useRef(333); - console.log(ref); - return React.createElement( - ctx.Provider, - { value: 0, ref: ref }, - React.createElement(context_user) - ); + let ref = React.useRef(333); + console.log(ref); + return React.createElement( + ctx.Provider, + { value: 0, ref: ref }, + React.createElement(context_user), + ); }); var app = () => { - return React.createElement("script", { - "aria-hidden": "true", - }, `console.log("asdfas");`); + return React.createElement( + "script", + { + "aria-hidden": "true", + }, + `console.log("asdfas");`, + ); }; -console.log(ReactDOM.renderToStaticMarkup(React.createElement(app, null))); +function App() { + let value = "asdfasdf"; + return ; +} + +console.log(ReactDOM.renderToStaticMarkup()); diff --git a/arch/server/render-html-to-stream.js b/arch/server/render-html-to-stream.js index 470412546..380de775f 100644 --- a/arch/server/render-html-to-stream.js +++ b/arch/server/render-html-to-stream.js @@ -1,5 +1,5 @@ import React, { Suspense } from "react"; -import ReactDOM from "react-dom/server"; +import * as ReactDOM from "react-dom/server"; const sleep = (seconds) => new Promise((res) => setTimeout(res, seconds * 1000)); @@ -61,7 +61,7 @@ const App = () => ( ); */ -function App() { +/* function App() { return React.createElement( Suspense, { fallback: "Fallback 1" }, @@ -77,8 +77,32 @@ function App() { ) ) ); - } + } */ + +/* function App() { + return ( + + + "asdf" +
lol
+ + + ); +} + */ + +function App() { + let value = "asdfasdf"; + return ( + {}} + /> + ); +} -ReactDOM.renderToReadableStream().then((stream) => { +ReactDOM.renderToReadableStream(, {}).then((stream) => { debug(stream); }); diff --git a/arch/server/render-rsc-to-stream.js b/arch/server/render-rsc-to-stream.js index e789ec5af..94111108c 100644 --- a/arch/server/render-rsc-to-stream.js +++ b/arch/server/render-rsc-to-stream.js @@ -3,7 +3,11 @@ import { renderToPipeableStream } from "react-server-dom-webpack/server"; const DefferedComponent = async ({ sleep, children }) => { await new Promise((res) => setTimeout(() => res(), sleep * 1000)); - return Sleep {sleep}s, {children}; + return ( + + Sleep {sleep}s, {children} + + ); }; const decoder = new TextDecoder(); @@ -25,7 +29,7 @@ const debug = (readableStream) => { const sleep = (seconds) => new Promise((res) => setTimeout(res, seconds * 1000)); -const App = () => ( +/* const App = () => ( @@ -34,6 +38,13 @@ const App = () => ( ); + */ + +function App() { + let value = "asdfasdf"; + let onChange = () => {}; + return ; +} const { pipe } = renderToPipeableStream(); diff --git a/benchmark/main.re b/benchmark/main.re index ffa01ab68..470e60d80 100644 --- a/benchmark/main.re +++ b/benchmark/main.re @@ -33,12 +33,14 @@ let run = (tests: list(Bench.Test.t)) => { let test_component = Bench.Test.create(~name="renderToStaticMarkup_component", () => { - ignore(ReactDOM.renderToStaticMarkup()) + let _element = ReactDOM.renderToStaticMarkup(); + (); }); let test_app = Bench.Test.create(~name="renderToStaticMarkup_app", () => { - ignore(ReactDOM.renderToStaticMarkup()) + let _element = ReactDOM.renderToStaticMarkup(); + (); }); run([test_component, test_app]); diff --git a/benchmark/once.ml b/benchmark/once.ml index 22571b901..697b6ecee 100644 --- a/benchmark/once.ml +++ b/benchmark/once.ml @@ -38,7 +38,7 @@ let main () = in let render_hello_world () = - let _ = ReactDOM.renderToStaticMarkup (HelloWorld.make ()) in + let _ = ReactDOM.renderToStaticMarkup (Static_small.make ()) in () in let render_app () = diff --git a/demo/client/index.re b/demo/client/Hydrate.re similarity index 73% rename from demo/client/index.re rename to demo/client/Hydrate.re index 47a9f609e..aadbca1a7 100644 --- a/demo/client/index.re +++ b/demo/client/Hydrate.re @@ -1,12 +1,13 @@ -let%browser_only mockInitWebsocket = () => [%mel.raw - {| +let%browser_only mockInitWebsocket: unit => unit = + () => [%mel.raw + {| function mockInitWebsocket() { console.log("Load JS"); } |} -]; + ]; -let _ = mockInitWebsocket(); +mockInitWebsocket(); let element = Webapi.Dom.Document.querySelector("#root", Webapi.Dom.document); diff --git a/demo/client/build.mjs b/demo/client/build.mjs index ac04c25ef..b79973d1f 100644 --- a/demo/client/build.mjs +++ b/demo/client/build.mjs @@ -1,66 +1,65 @@ -import esbuild from 'esbuild'; -import { plugin as extractClientComponents } from '../../packages/extract-client-components/esbuild-plugin.mjs'; +import esbuild from "esbuild"; +import { plugin as extractClientComponents } from "../../packages/extract-client-components/esbuild-plugin.mjs"; async function build(input, output, extract) { - let outdir = undefined; - let outfile = undefined; - let splitting = false; + let outdir = undefined; + let outfile = undefined; + let splitting = false; - /* shitty way to check if output is a directory or a file */ - if (output.endsWith('/')) { - outdir = output; - splitting = true; - } else { - outfile = output; - splitting = false; - } + /* shitty way to check if output is a directory or a file */ + if (output.endsWith("/")) { + outdir = output; + splitting = true; + } else { + outfile = output; + splitting = false; + } - let plugins = []; - if (extract) { - plugins.push(extractClientComponents({ target: 'app' })); - } + let plugins = []; + if (extract) { + plugins.push(extractClientComponents({ target: "app" })); + } - try { - const result = await esbuild.build({ - entryPoints: [input], - bundle: true, - platform: 'browser', - format: 'esm', - splitting, - logLevel: 'error', - outdir: outdir, - outfile: outfile, - plugins, - write: true, - metafile: true, - }); + try { + const result = await esbuild.build({ + entryPoints: [input], + bundle: true, + logLevel: "debug", + platform: "browser", + format: "esm", + splitting, + outdir: outdir, + outfile: outfile, + plugins, + write: true, + }); - console.log('Build completed successfully for "' + input + '"'); - return result; - } catch (error) { - console.error('\nBuild failed:', error); - process.exit(1); - } + console.log('Build completed successfully for "' + input + '"'); + return result; + } catch (error) { + console.error("\nBuild failed:", error); + process.exit(1); + } } const input = process.argv[2]; const output = process.argv[3]; let parseExtract = (arg) => { - if (typeof arg == "string") { - if (arg.startsWith("--extract=")) { - return arg.split("--extract=")[1] === "true"; - } - } + if (typeof arg == "string") { + if (arg.startsWith("--extract=")) { + return arg.split("--extract=")[1] === "true"; + } + } - return false; + return false; }; const extract = parseExtract(process.argv[4]); if (!input) { - console.error('Please provide an input file path'); - process.exit(1); + console.error("Please provide an input file path"); + process.exit(1); } build(input, output, extract); diff --git a/demo/client/create-from-fetch.jsx b/demo/client/create-from-fetch.jsx index 59239f6d6..ad30b195d 100644 --- a/demo/client/create-from-fetch.jsx +++ b/demo/client/create-from-fetch.jsx @@ -1,21 +1,21 @@ window.__webpack_require__ = () => { - throw new Error("__webpack_require__ should not be called on this demo"); + throw new Error("__webpack_require__ should not be called on this demo"); }; let ReactDOM = require("react-dom/client"); let ReactServerDOM = require("react-server-dom-webpack/client"); let fetchXcomponent = () => { - return fetch("/demo/server-components-without-client", { - method: 'GET', - headers: { - Accept: "text/x-component", - }, - }); + return fetch("/demo/server-components-without-client", { + method: "GET", + headers: { + Accept: "text/x-component", + }, + }); }; let root = ReactDOM.createRoot(document.getElementById("root")); -ReactServerDOM.createFromFetch(fetchXcomponent()).then(element => { - root.render(element); +ReactServerDOM.createFromFetch(fetchXcomponent()).then((element) => { + root.render(element); }); diff --git a/demo/client/create-from-readable-stream.jsx b/demo/client/create-from-readable-stream.jsx index 919c745ad..384f28d8c 100644 --- a/demo/client/create-from-readable-stream.jsx +++ b/demo/client/create-from-readable-stream.jsx @@ -2,34 +2,8 @@ const React = require("react"); const ReactDOM = require("react-dom/client"); const ReactServerDOM = require("react-server-dom-webpack/client"); -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError(error) { - // Update state so the next render will show the fallback UI. - return { hasError: true }; - } - - componentDidCatch(error, errorInfo) { - // You can also log the error to an error reporting service - console.error(error, errorInfo); - } - - render() { - if (this.state.hasError) { - return

Something went wrong

; - } - - return this.props.children; - } -} - function Use({ promise }) { - const tree = React.use(promise); - return tree; + return React.use(promise); } try { @@ -38,11 +12,7 @@ try { const element = document.getElementById("root"); React.startTransition(() => { - const app = ( - - - - ); + const app = ; ReactDOM.hydrateRoot(element, app); }); } catch (e) { diff --git a/demo/client/dune b/demo/client/dune index 52c9a66d8..349952b7b 100644 --- a/demo/client/dune +++ b/demo/client/dune @@ -1,10 +1,10 @@ -;; index.re +;; hydrate.re (melange.emit (enabled_if (= %{profile} dev)) (target app) - (modules index) + (modules hydrate) (module_systems (es6 js)) (libraries melange reason-react melange.dom melange-webapi demo_shared_js) @@ -19,11 +19,11 @@ (package server-reason-react) (alias_rec melange) (:script build.mjs) - (:input app/demo/client/index.js) + (:input "app/demo/client/Hydrate.js") (source_tree node_modules) (file package.json) (source_tree ../../packages/extract-client-components)) - (target index.js) + (target hydrate-static-html.js) (action (progn (run node %{script} %{input} %{target})))) @@ -62,3 +62,22 @@ (action (progn (run node %{script} %{input} app/demo/client/ --extract=true)))) + +;; router.jsx + +(rule + (enabled_if + (= %{profile} dev)) + (alias client) + (deps + (package server-reason-react) + (alias_rec melange) + (:input router.jsx) + (:script build.mjs) + (file package.json) + (source_tree node_modules) + (source_tree ../../demo/universal/native) + (source_tree ../../packages/extract-client-components)) + (action + (progn + (run node %{script} %{input} app/demo/client/ --extract=true)))) diff --git a/demo/client/package-lock.json b/demo/client/package-lock.json index cb4731aa1..e374866f9 100644 --- a/demo/client/package-lock.json +++ b/demo/client/package-lock.json @@ -9,12 +9,159 @@ "version": "0.0.0", "license": "ISC", "dependencies": { + "@biomejs/biome": "^1.9.4", "esbuild": "^0.21.4", "react": "^19.0.0-rc-69d4b800-20241021", "react-dom": "^19.0.0-rc-69d4b800-20241021", "react-server-dom-webpack": "^19.0.0-rc-69d4b800-20241021" } }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "hasInstallScript": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1405,6 +1552,69 @@ } }, "dependencies": { + "@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "requires": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "optional": true + }, + "@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "optional": true + }, + "@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "optional": true + }, + "@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "optional": true + }, + "@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "optional": true + }, + "@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "optional": true + }, + "@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "optional": true + }, + "@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "optional": true + }, "@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", diff --git a/demo/client/router.jsx b/demo/client/router.jsx new file mode 100644 index 000000000..b54037046 --- /dev/null +++ b/demo/client/router.jsx @@ -0,0 +1,144 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import ReactServerDOM from "react-server-dom-webpack/client"; + +let updateRoot = null; +let abortController = null; + +function Page({ data }) { + // Store the current root element in state, along with a callback + // to call once rendering is complete. + let [[root, cb], setRoot] = React.useState([React.use(data), null]); + updateRoot = (root, cb) => setRoot([root, cb]); + React.useInsertionEffect(() => cb?.()); + return root; +} + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // You can also log the error to an error reporting service + console.error(error, errorInfo); + } + + render() { + if (this.state.hasError) { + return

Something went wrong

; + } + + return this.props.children; + } +} +const App = ({ data }) => { + return ( + + + + ); +}; + +const callServer = (_id, _args) => { + throw new Error(`callServer is not supported yet`); +}; + +const element = document.getElementById("root"); +const stream = window.srr_stream && window.srr_stream.readable_stream; + +if (stream) { + console.log("__client_manifest_map", window.__client_manifest_map); + const data = ReactServerDOM.createFromReadableStream(stream, { callServer }); + React.startTransition(() => { + const app = ; + ReactDOM.hydrateRoot(element, app); + }); +} else { + /* when does stream not exist? */ + let { pathname, search } = window.location; + navigate(pathname + search); +} + +// Simple router's navigation. On navigate, fetch the RSC payload from the server, +// and in a React transition, stream in the new page. Once complete, we'll pushState to +// update the URL in the browser. +async function navigate(search) { + let queryStrings = "?" + search; + if (window.location.search === queryStrings) { + return; + } + console.log("navigate", search); + let origin = window.location.origin; + let pathname = window.location.pathname; + console.log("pathname", pathname); + let url = new URL(origin + pathname + queryStrings); + if (abortController != null) { + abortController.abort(); + } + abortController = new AbortController(); + let res = fetch(url.toString(), { + headers: { + Accept: "application/react.component", + }, + signal: abortController.signal, + }); + let root = await ReactServerDOM.createFromFetch(res); + React.startTransition(() => { + updateRoot(root, () => { + history.pushState(null, "", url.pathname + url.search); + }); + }); +} + +/* function useAction(endpoint, method) { + console.log("useAction", endpoint, method); + const { refresh } = useRouter(); + const [isSaving, setIsSaving] = React.useState(false); + const [didError, setDidError] = React.useState(false); + const [error, setError] = React.useState(null); + + if (didError) { + // Let the nearest error boundary handle errors while saving. + throw error; + } + + async function performMutation(payload, requestedLocation) { + setIsSaving(true); + try { + const response = await fetch( + `${endpoint}?location=${encodeURIComponent( + JSON.stringify(requestedLocation), + )}`, + { + method, + body: JSON.stringify(payload), + headers: { + "Content-Type": "application/json", + }, + }, + ); + if (!response.ok) { + throw new Error(await response.text()); + } + refresh(response); + } catch (e) { + setDidError(true); + setError(e); + } finally { + setIsSaving(false); + } + } + + return [performMutation, isSaving]; +} */ + +/* Publish navigate to window, to avoid circular dependency. Once the implementation of router is migrated into a library, we can remove this and use "navigate" directly */ +window.__navigate = navigate; +/* window.__useAction = useAction; */ diff --git a/demo/server/DB.re b/demo/server/DB.re new file mode 100644 index 000000000..883fafaf6 --- /dev/null +++ b/demo/server/DB.re @@ -0,0 +1,86 @@ +open Lwt.Syntax; + +let readFile = file => { + let (/) = Filename.concat; + let path = Sys.getcwd() / "demo" / "server" / "db" / file; + switch%lwt (Lwt_io.with_file(~mode=Lwt_io.Input, path, Lwt_io.read)) { + | v => Lwt_result.return(v) + | exception e => + Dream.log("Error reading file %s: %s", path, Printexc.to_string(e)); + Lwt.return_error(Printexc.to_string(e)); + }; +}; + +let parseNote = (note: Yojson.Safe.t): option(Note.t) => + switch (note) { + | `Assoc(fields) => + let id = + fields |> List.assoc("id") |> Yojson.Safe.to_string |> int_of_string; + let title = fields |> List.assoc("title") |> Yojson.Safe.Util.to_string; + let content = + fields |> List.assoc("content") |> Yojson.Safe.Util.to_string; + let updated_at = + fields + |> List.assoc("updated_at") + |> Yojson.Safe.to_string + |> float_of_string; + Some({ + id, + title, + content, + updated_at, + }); + | _ => None + }; + +let parseNotes = json => { + switch (Yojson.Safe.from_string(json)) { + | `List(notes) => notes |> List.filter_map(parseNote) |> Result.ok + | _ => Result.error("Invalid notes file format") + | exception _ => Result.error("Invalid JSON format format") + }; +}; + +module Cache = { + let db_cache = ref(None); + let set = value => db_cache := Some(value); + let read = () => db_cache^; + let delete = () => db_cache := None; +}; + +let readNotes = () => { + switch (Cache.read()) { + | Some(Ok(notes)) => Lwt_result.return(notes) + | Some(Error(e)) => Lwt_result.fail(e) + | None => + switch%lwt (readFile("./notes.json")) { + | Ok(json) => + Cache.set(parseNotes(json)); + Lwt_result.lift(parseNotes(json)); + | Error(e) => Lwt.return_error("Error reading notes file") + } + /* When something fails, treat it as an empty note db */ + | exception _error => Lwt.return_ok([]) + }; +}; + +let findOne = (notes, id) => { + switch (notes |> List.find_opt((note: Note.t) => note.id == id)) { + | Some(note) => Lwt_result.return(note) + | None => + Lwt_result.fail("Note with id " ++ Int.to_string(id) ++ " not found") + }; +}; + +let fetchNote = id => { + switch (Cache.read()) { + | Some(Ok(notes)) => findOne(notes, id) + | Some(Error(e)) => Lwt_result.fail(e) + | None => + let* notes = readNotes(); + switch (notes) { + | Ok(notes) => findOne(notes, id) + | Error(e) => Lwt_result.fail(e) + }; + }; +}; diff --git a/demo/server/Date.re b/demo/server/Date.re new file mode 100644 index 000000000..7c69ec590 --- /dev/null +++ b/demo/server/Date.re @@ -0,0 +1,30 @@ +let is_today = date => { + let now = Unix.localtime(Unix.time()); + let d = Unix.localtime(date); + now.tm_year == d.tm_year + && now.tm_mon == d.tm_mon + && now.tm_mday == d.tm_mday; +}; + +let format_time = date => { + let t = Unix.localtime(date); + let hour = t.tm_hour mod 12; + let hour = + if (hour == 0) { + 12; + } else { + hour; + }; + let ampm = + if (t.tm_hour >= 12) { + "pm"; + } else { + "am"; + }; + Printf.sprintf("%d:%02d %s", hour, t.tm_min, ampm); +}; + +let format_date = date => { + let t = Unix.localtime(date); + Printf.sprintf("%d/%d/%02d", t.tm_mon + 1, t.tm_mday, t.tm_year mod 100); +}; diff --git a/demo/server/DreamRSC.re b/demo/server/DreamRSC.re new file mode 100644 index 000000000..13779f3dd --- /dev/null +++ b/demo/server/DreamRSC.re @@ -0,0 +1,110 @@ +let is_react_component_header = str => + String.equal(str, "application/react.component"); + +let stream_rsc = (request, fn) => { + Dream.stream( + ~headers=[ + ("Content-Type", "application/react.component"), + ("X-Content-Type-Options", "nosniff"), + ("X-Location", Dream.target(request)), + ], + stream => { + let%lwt () = fn(stream); + Lwt.return(); + }, + ); +}; + +let render_shell = (app, script) => { + let doctype = Html.raw(""); + let head = children => { + Html.node( + "head", + [], + [ + Html.node("meta", [Html.attribute("charset", "utf-8")], []), + Html.node("title", [], [Html.string("React Server DOM")]), + ...children, + ], + ); + }; + let sync_scripts = + Html.node( + "script", + [Html.attribute("src", "https://cdn.tailwindcss.com")], + [], + ); + let async_scripts = + Html.node( + "script", + [ + Html.attribute("src", script), + Html.attribute("async", "true"), + Html.attribute("type", "module"), + ], + [], + ); + let headers = [("Content-Type", "text/html")]; + Dream.stream(~headers, stream => { + switch%lwt (ReactServerDOM.render_html(app)) { + | ReactServerDOM.Done({head: head_children, body, end_script}) => + Dream.log("Done: %s", Html.to_string(body)); + let%lwt () = Dream.write(stream, Html.to_string(doctype)); + let%lwt () = + Dream.write( + stream, + Html.to_string(head([sync_scripts, async_scripts, head_children])), + ); + let%lwt () = Dream.write(stream, "
"); + let%lwt () = Dream.write(stream, Html.to_string(body)); + let%lwt () = Dream.write(stream, "
"); + let%lwt () = Dream.write(stream, Html.to_string(end_script)); + let%lwt () = Dream.write(stream, ""); + Dream.flush(stream); + | ReactServerDOM.Async({head: head_children, shell: body, subscribe}) => + let%lwt () = Dream.write(stream, Html.to_string(doctype)); + let%lwt () = + Dream.write( + stream, + Html.to_string(head([sync_scripts, async_scripts, head_children])), + ); + let%lwt () = Dream.write(stream, "
"); + let%lwt () = Dream.write(stream, Html.to_string(body)); + let%lwt () = Dream.write(stream, "
"); + let%lwt () = Dream.flush(stream); + let%lwt () = + subscribe(chunk => { + Dream.log("Chunk"); + Dream.log("%s", Html.to_string(chunk)); + let%lwt () = Dream.write(stream, Html.to_string(chunk)); + Dream.flush(stream); + }); + let%lwt () = Dream.write(stream, ""); + Dream.flush(stream); + } + }); +}; + +let createFromRequest = (app, script, request) => { + switch (Dream.header(request, "Accept")) { + | Some(accept) when is_react_component_header(accept) => + stream_rsc( + request, + stream => { + let%lwt _stream = + ReactServerDOM.render_model( + app, + ~subscribe=chunk => { + Dream.log("Chunk"); + Dream.log("%s", chunk); + let%lwt () = Dream.write(stream, chunk); + Dream.flush(stream); + }, + ); + + Dream.flush(stream); + }, + ) + | _ => render_shell(app, script) + }; +}; diff --git a/demo/server/Error.re b/demo/server/Error.re deleted file mode 100644 index 0445162bc..000000000 --- a/demo/server/Error.re +++ /dev/null @@ -1,31 +0,0 @@ -let handler = - Dream.error_template((error, info, suggested) => { - let status = Dream.status(suggested); - let code = Dream.status_to_int(status); - let reason = Dream.status_to_string(status); - Dream.html( - ReactDOM.renderToStaticMarkup( - -
-
- -

- {React.string(reason)} -

-
-
-                
-                  {React.string(info)}
-                
-              
-
-
-
, - ), - ); - }); diff --git a/demo/server/Home.re b/demo/server/Home.re deleted file mode 100644 index f0f2641b5..000000000 --- a/demo/server/Home.re +++ /dev/null @@ -1,19 +0,0 @@ -let handler = _request => { - let app = - -
- -

- {React.string("Home of the demos")} -

-
- -
-
; - - Dream.html(ReactDOM.renderToStaticMarkup(app)); -}; diff --git a/demo/server/Markdown.re b/demo/server/Markdown.re new file mode 100644 index 000000000..b54ed8900 --- /dev/null +++ b/demo/server/Markdown.re @@ -0,0 +1,237 @@ +module List = { + include List; + let rec [@tailcall] take = + (lst, n) => + switch (lst, n) { + | ([], _) => [] + | (_, 0) => [] + | ([x, ...xs], n) => [x, ...take(xs, n - 1)] + }; +}; + +let convert_headings = text => { + text + |> Str.global_replace(Str.regexp("^#### \\(.*\\)$"), "

\\1

") + |> Str.global_replace(Str.regexp("^### \\(.*\\)$"), "

\\1

") + |> Str.global_replace(Str.regexp("^## \\(.*\\)$"), "

\\1

") + |> Str.global_replace(Str.regexp("^# \\(.*\\)$"), "

\\1

"); +}; + +let convert_emphasis = text => { + text + |> Str.global_replace( + Str.regexp("\\*\\*\\([^*]*\\)\\*\\*"), + "\\1", + ) + |> Str.global_replace( + Str.regexp("__\\([^_]*\\)__"), + "\\1", + ) + |> Str.global_replace(Str.regexp("\\*\\([^*]*\\)\\*"), "\\1") + |> Str.global_replace(Str.regexp("_\\([^_]*\\)_"), "\\1"); +}; + +let convert_code = text => { + text + |> Str.global_replace( + Str.regexp("```\\([^`]*\\)```"), + "
\\1
", + ) + |> Str.global_replace(Str.regexp("`\\([^`]*\\)`"), "\\1"); +}; + +let convert_links = text => { + text + |> Str.global_replace( + Str.regexp("\\[\\([^]]*\\)\\](\\([^)]*\\))"), + "\\1", + ); +}; + +let convert_lists = text => { + let lines = String.split_on_char('\n', text); + + let process_line = line => { + switch (line) { + | line when Str.string_match(Str.regexp("^-\\s*\\(.*\\)$"), line, 0) => + "
  • " ++ Str.matched_group(1, line) ++ "
  • " + | line when Str.string_match(Str.regexp("^\\+\\s*\\(.*\\)$"), line, 0) => + "
  • " ++ Str.matched_group(1, line) ++ "
  • " + | line when Str.string_match(Str.regexp("^\\*\\s*\\(.*\\)$"), line, 0) => + "
  • " ++ Str.matched_group(1, line) ++ "
  • " + | line + when Str.string_match(Str.regexp("^\\d+\\.\\s*\\(.*\\)$"), line, 0) => + "
  • " ++ Str.matched_group(1, line) ++ "
  • " + | _ => line + }; + }; + + let wrap_consecutive_items = lines => { + let rec aux = (acc, current_list, lines) => { + switch (current_list, lines) { + | ([], []) => List.rev(acc) + | ([hd, ...tl], []) => + List.rev([ + "
      " ++ String.concat("\n", List.rev([hd, ...tl])) ++ "
    ", + ...acc, + ]) + | ([], [line, ...rest]) => + if (Str.string_match(Str.regexp("^
  • "), line, 0)) { + aux(acc, [line], rest); + } else { + aux([line, ...acc], [], rest); + } + | ([hd, ...tl] as items, [line, ...rest]) => + if (Str.string_match(Str.regexp("^
  • "), line, 0)) { + aux(acc, [line, ...current_list], rest); + } else { + aux( + [ + line, + "
      " ++ String.concat("\n", List.rev(items)) ++ "
    ", + ...acc, + ], + [], + rest, + ); + } + }; + }; + aux([], [], lines); + }; + + lines + |> List.map(process_line) + |> wrap_consecutive_items + |> String.concat("\n"); +}; + +let wrap_lists = text => { + text + |> Str.global_replace( + Str.regexp("
  • .*
  • \\(\n
  • .*
  • \\)*"), + "
      \\0
    ", + ); +}; + +let convert_blockquotes = text => { + let lines = String.split_on_char('\n', text); + + let rec process_lines = (acc, in_quote, lines) => { + switch (lines) { + | [] when in_quote => List.rev(["", ...acc]) + | [] => List.rev(acc) + | [line, ...rest] => + let trimmed = String.trim(line); + if (Str.string_match(Str.regexp("^>\\s*\\(.*\\)$"), trimmed, 0)) { + let content = Str.matched_group(1, trimmed); + if (in_quote) { + process_lines([content, ...acc], true, rest); + } else { + process_lines([content, "
    ", ...acc], true, rest); + }; + } else if (trimmed == "") { + if (in_quote) { + process_lines(["
    ", ...acc], false, rest); + } else { + process_lines([line, ...acc], false, rest); + }; + } else if (in_quote) { + process_lines([line, ...acc], true, rest); + } else { + process_lines([line, ...acc], false, rest); + }; + }; + }; + + lines |> process_lines([], false) |> String.concat("\n"); +}; + +let convert_paragraphs = text => { + let lines = String.split_on_char('\n', text); + + let is_block_element = line => + Str.string_match( + Str.regexp("^<\\(h[1-6]\\|ul\\|ol\\|blockquote\\|pre\\)>"), + line, + 0, + ); + + let wrap_paragraphs = lines => { + let rec aux = (acc, current_p, lines) => { + switch (lines) { + | [] when current_p != "" => + List.rev(["

    " ++ current_p ++ "

    ", ...acc]) + | [] => List.rev(acc) + | [line, ...rest] when is_block_element(line) => + if (current_p != "") { + aux([line, "

    " ++ current_p ++ "

    ", ...acc], "", rest); + } else { + aux([line, ...acc], "", rest); + } + | [line, ...rest] when String.trim(line) == "" => + if (current_p != "") { + aux(["

    " ++ current_p ++ "

    ", ...acc], "", rest); + } else { + aux(acc, "", rest); + } + | [line, ...rest] => + let sep = + if (current_p == "") { + ""; + } else { + " "; + }; + aux(acc, current_p ++ sep ++ String.trim(line), rest); + }; + }; + aux([], "", lines); + }; + + lines |> wrap_paragraphs |> String.concat("\n"); +}; + +let toHTML = markdown => { + markdown + |> convert_headings + |> convert_emphasis + |> convert_code + |> convert_links + |> convert_lists + |> wrap_lists + |> convert_blockquotes + |> convert_paragraphs + |> String.trim; +}; + +let extract_text = markdown => { + markdown + |> Str.global_replace(Str.regexp("\\[([^]]*)\\]\\([^)]*\\)"), "\\1") + |> Str.global_replace(Str.regexp("\\*\\*\\([^*]*\\)\\*\\*"), "\\1") + |> Str.global_replace(Str.regexp("\\*\\([^*]*\\)\\*"), "\\1") + |> Str.global_replace(Str.regexp("__\\([^_]*\\)__"), "\\1") + |> Str.global_replace(Str.regexp("_\\([^_]*\\)_"), "\\1") + |> Str.global_replace(Str.regexp("~~\\([^~]*\\)~~"), "\\1") + |> Str.global_replace(Str.regexp("`\\([^`]*\\)`"), "\\1") + |> Str.global_replace(Str.regexp("```[^`]*```"), "") + |> Str.global_replace(Str.regexp("^#+ .*$"), "\n") + |> Str.global_replace(Str.regexp("^#* .*$"), "\n") + |> Str.global_replace(Str.regexp("> \\|>"), "") + |> Str.global_replace(Str.regexp("\\[\\|\\]\\|\\(\\|\\)"), "") + |> Str.global_replace(Str.regexp("-\\|\\+\\|\\*\\s+"), "") + |> Str.global_replace(Str.regexp("^\\d+\\.\\s+"), "") + |> Str.global_replace(Str.regexp("\\\\"), "") + |> String.trim; +}; + +let summarize = (text, ~words as n) => { + let words = Str.split(Str.regexp("[ \n\r\t]+"), text); + let truncated = List.take(words, n); + let dots = + if (List.length(words) > n) { + "..."; + } else { + ""; + }; + String.concat(" ", truncated) ++ dots; +}; diff --git a/demo/server/db/notes.json b/demo/server/db/notes.json new file mode 100644 index 000000000..8529c93f2 --- /dev/null +++ b/demo/server/db/notes.json @@ -0,0 +1,26 @@ +[ + { + "id": 0, + "title": "Lorem ipsum for markdown, exists", + "content": "# Aethere conterminus nec est damno\n\nLorem markdownum patiente clade retenta, domos facta cacumine nostris coniunx aspergine intraverat. Petit **et**, est est recens invitaque refert asper vigoris undis sacerdos.\n\nEt undis laetos Caystros intellege est auras corpus, montes ambit tum formae pellitis [et inque](#lenta-argo). Sit dicentum nondum, Dorceus debita attonitum nulla cornua vestem si auras.\n\n## Dummodo in veretur argenti plenissima quoque damnare\n\nSic quae, aula fortibus fratribus longoque abiit mea cava commune spectant uno telis hiemem. Quibus vestigia pugnat prolisque Caesareos bracchia caesae, victoria citi colubris totos penates usum hirta. Perituraque inest promittat Procris mille famaque ursa, hamadryadas rapuere moxque amorem. Domui comites adspicit tabellae euhoe matri duxere dei Dianae Aegyptia celebrare. Veniat gestasset levavit: oras cursus arcebatque quid, herba caput tum praecepta.\n\nUrget quod dixi idemque. Timuitque hortis dubiaque meo per cantu admissum manibus lapis minimamque simulacraque currere licet, Fortuna reliquit massa. Positus tenet.\n\nLusisse Hymenaeon terrore referebat mortale in Pelopeia, facienda positoque bibisset. Per totum, virginibus dumque cornua modumque domus arma ecquid hoc meo [tertius hic](#mater). Coniugis laudis, *fertur*, postquam nostro, mihi mortale fessam illa quater autumni per sapiens, albentes hippomene, et.\n\n## Non sacra tibi superare circa\n\nEst per viro est, nec in trunca causa. Viso placet, cadunt quaque ignorant verbenis loquor exceptas in, summe iuga nectare. Mihi domus segetes, ferro, in quodque litoraque dixit, mediis bacis, egit, qua meae iram Boeotaque.\n\nCuspide accipit poterat, spreto haec quoque? Turpe quae, iacentia esset fissa vivum, an lacertis ire; spumas. Quis quod concidit Alcmene. Attollo mollia metalla adest terris **cultosque prompsit celsum** minima, saepe. Et mi est laverat totis, videndo dedimus capientur: iamque.\n\nHumanas nec, adest hanc iaculum; Phrygiae vae deinde quae neque quodque Nesse caelum chlamydem tamen generosos? Genuit puer placabilis tamen, invito et nervos tuam hoc.\n\nOpus nec. Motu omne vates negate fluere, nec sic membra Hennaeis pleno, arcana toto non. Quicquid se opta saepe! Tibi litis sunt saliente herba. Stamina huius sceptra iuvenes turbasti et mihi votique qua tanta, uno super ero vacuus fluminis tepida.", + "updated_at": 1716604800 + }, + { + "id": 1, + "title": "Our markdown parser is poor, don't stress it plz", + "content": "\n# Murra acta una cretum refert\n## Undas pati incola cognoscit Arethusa\nPatrium utentem: illa tempora; reddit seque, ab fuga notas Charaxi! Mater bibulas o mollia elisi veribus,\n[virtute](http: //www.de.com/iuncta-iactanti.html) tutus sub nam strictumque gens animasse,\n[anni inde](http: //aera.org/) illuc tellus. Munus generosos militis quoque sit.\n## Semper plura tempora tantae effodit cervix subito\nUllam vero: duris mea bellis pulvere! Cervice placidi, ignes, Laelapa pectore languentique fugitque; utroque Medusaeo. Pereat des, argento gemmas praestantes Amore referre. Tamen lanugine novercae frigora miser cum.\nPallas iam solet salix transit; causa fugio animalibus currant verba aversata, faciam tenus, unda. Opem tereti lecto ferventibus pater: Festa fictus nihil: mea quidem quem quodque leonis ad urbes: deus? Tantae corpus; o audit vox animam peteret presso sua quatere Venus.\n## Quem et recondidit puer conlapsa currus\nCeycis mallem bracchia. Minimas si invitae et catulo in detestatur fuit, dea pares, viscera flebant Elin solet annis frondescere sordidus. Laborem ut Troezena grandia at certans posses et Minyeides **nobis** tracti natae.\n## Posse ulla templo Iove aliter\nPictae aeraque sceptro, stupet, levi nec amans hoc breve inplet, gravidamque, locus. Foribusque simul, caput amplexa, silvis titulis: e removit aderat: orbae frenis, ingenium ardua gradere **esse**. Sine et *pessima percusso* est tener aduncis funibus claro: *sumptis* hostibus et, ore venti iamdudum. Laniare novis scopulis, tu priora veniens, nec quodque se novorum tribuam nomine?\n\n## Quodque cervicibus luces\nDedit socios esset, exarsit et movere Saturnia pudici, herede. Nec optima, non hanc spisso, sum gladii qua descendunt **noceat altoque me**. Patrium utentem: illa tempora; reddit seque, ab fuga notas Charaxi.\nPede tota, ligati: subduci succedit animas recessit inde aut, salva, ista. Artes carebat nutrix, arte primus sceptra accipis subit manibusque.\n> Rudentes quaque nec error tecta aves sic obsistere, ignis non nisi expalluit quater harpen; domus. Sine exilibus caerula quo modico et imagine, cana cognoscenda pars torvum cupidine membra: Achilli negabamus manu nec? Tenebant et rubigine tremuere deorum, ora. [Quae agmine patriaeque](http: //incurvatasortemque.io/) fuerit obverterat quoque; sum reticere; huic quaque **adspergine exsangues**! Protegat **verso** fama limite [ligno dextera](http: //intendens-in.net/lentinavis.html), lusisse at haeserat > pro exarsit deae: magni hamo altior.\n", + "updated_at": 1716604800 + }, + { + "id": 2, + "title": "Another important note", + "content": "# Ad Latous\n\n## Verba nostra\n\nRemovere vicimus quid nisi fluctibus Dictys. Tutus ictu amborum iniere inque, quod, omnes, neu pariter Andraemone nequiquam quod suo; luctantia. Feris ara fusum reliquit spirat longique alitibus, ab capillis movi persequitur.\n\n## Crimina Fames\n\nErat qui quodque decusque te tibi nil volucrem [in audaci](#in-et) obliquis **rebus tacuit**. Virtute est annis arma aequora, tenet vellem Eurydices dixerunt supplice animal.\n\n## Ab ego saxum ab tecta tympana mentita\n\nMei mutabit lacerata. Voti aguntur teloque, adest *vocabant*, unde defecto habet.\n\n- Ait pondere\n- Flamma putares cursu genitore plagas conabar manibus\n- Guttis recepta\n- Dixit electus\n- Exaudi tremulo\n- Natique duroque intrat sperat\n\n## Damnarat velox acerris mihi invitus celebrantur mali\n\nRemovere vicimus quid nisi. Est sed in neque patietur foramen exi haec ait. Ter laverat sociis quasque potitur si [est columnis](#dominoque) tempora dum audito et omnia *Pharosque est*.\n\n```cgiKeyboard(myspace_blacklist_streaming.ebook.browserUatBcc(5 / 45, ofRwSecondary), ccd);```\n\nFunda **Gorgoneum tenera ardet** condar viros cannae sequiturque *claro* quicumque. Serpens innumeraeque Cereris foret agitat socios gravem aquae nescisse, deus acie. Demissaque unum dubitabile erepta sanguinea surgis scindere illic meae credidit **dummodo maius** dat aures Illyricos coercet concipe roganti repetitaque.\n", + "updated_at": 1716804800 + }, + { + "id": 3, + "title": "RSC with navigation, yuhu! 🎉", + "content": "# Scrobibus luctu sunt cognoscenda erat iuvenis\n\n## Ciboque exuit quoque toris portae sed equos\n\nStygiam colle porrigit et stipite\ncuraque muneris. Aram labens admonitu prensam status, vox undis et\n**percussit**, quoque; nec mando ripae!\n\n## Per nec rudentes auras\n\nIxionis talia, nam de quaerere limine, illa non neu flevit! Suis cui nec esset\nquid crura. Quae fore uterum summa.\n\n## Populo refluitque deprensa\n\nPergama et fuerunt signa commemorare ecce, non ferit, impetus ab sustinui resto,\npiscem *inductus*, quem. Manum cruentior obruit. Alis Epiros; tum alti aurum\nvideri *et siqua tecta*, vitamque vellera quam superatus per matris mollescat,\n*si*.\n\nMei [signa](#satis-in-illo) evitata Elin flumina; divum\n[puer](#figuram-tot-vocari), reppulit ira arcton. Epulis ut incepti quod. Ter\naliis acta *ira*: obstitit!\n\n## Ipse forte ille remittat\n\nIpse que, nexu vana sequar fui opus perstant! Post hospes.\n\nUt quae illi vidit et in me, *sonumque coniunx* gravitate montis legum pars? Ait\ne addidit guttur, **habitantque** saxum Mopsopium innuba et Peragit sedisti et\nglaebis, ambitae quo currere. Ante per ignem; infantem inpositus tu enim qui:\nhostis mihi mirum euntem, quid? Spicis et frontem repressit deinde, ut residens\nbella vocatum [plumbum](#leto-per-his). Voce documenta stant, inhonorati viaeque\nvidet iterum sanguine, aras veste futuri, argumenta arcus milite non, non?\n\nMaenades Turnusque consulat morsu, sive *mille tenuere ossibus*. Amor duo, ecce\nimperio muneraque contemptus quodcumque quam tetigere tibi, petit, ubi aurumque\n**rogant**. Loca nubes colla ademit: cognoscenda atque. Funereum habebit dixit\nest gemitum viroque Megareus quibus: bracchia signa meus, filia; lucem decent\ntacito?\n", + "updated_at": 1716904800 + } +] diff --git a/demo/server/dune b/demo/server/dune index 80e7d55d6..5317d1e0f 100644 --- a/demo/server/dune +++ b/demo/server/dune @@ -1,7 +1,9 @@ +(include_subdirs qualified) + (executable - (name server) (enabled_if (= %{profile} "dev")) + (name server) (flags :standard -w -26-27) ; browser_only removes code form the server, making this warning necessary (libraries dream @@ -11,6 +13,7 @@ html js lwt.unix + str unix belt yojson) diff --git a/demo/server/comments.re b/demo/server/pages/Comments.re similarity index 55% rename from demo/server/comments.re rename to demo/server/pages/Comments.re index 4a47464af..7555afdf0 100644 --- a/demo/server/comments.re +++ b/demo/server/pages/Comments.re @@ -1,23 +1,3 @@ -module Spinner = { - let make = () => { -
    ; - }; -}; - module Post = { let make = () => {
    @@ -83,35 +63,56 @@ module Comments = { }; }; -[@react.component] -let make = () => { - -
    -
    -

    - {React.string("Rendering React.Suspense on the server")} -

    - -
    -

    { + +
    +
    +

    - {React.string("Comments")} -

    - }> - - -

    -

    {React.string("Thanks for reading!")}

    -
    -
    -
    ; + {React.string("Rendering React.Suspense on the server")} + + +
    +

    + {React.string("Comments")} +

    + }> + + +
    +

    {React.string("Thanks for reading!")}

    + + + ; + }; +}; + +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(~pipe, ); + + Lwt_stream.iter_s(pipe, stream); + }, + ); }; diff --git a/demo/server/pages/Home.re b/demo/server/pages/Home.re new file mode 100644 index 000000000..557d3d9ce --- /dev/null +++ b/demo/server/pages/Home.re @@ -0,0 +1,39 @@ +let handler = _request => { + let app = + +
    + +

    + {React.string("demo for server-reason-react")} +

    + + + "This is a list of links to all the demos for server-reason-react's features." + +
    + + "Useful to manual test. If you want to learn more about server-reason-react, check out the " + + + "documentation" + + " or " + + "source code" + +
    +
    + +
    +
    ; + + Dream.html(ReactDOM.renderToStaticMarkup(app)); +}; diff --git a/demo/server/pages/NoteItem.re b/demo/server/pages/NoteItem.re new file mode 100644 index 000000000..830894d91 --- /dev/null +++ b/demo/server/pages/NoteItem.re @@ -0,0 +1,64 @@ +open Lwt.Syntax; + +module NoteView = { + [@react.component] + let make = (~note: Note.t) => { +
    +
    +
    +

    + {React.string(note.title)} +

    + + {"Last updated on " ++ Date.format_date(note.updated_at)} + +
    + +
    + +
    ; + }; +}; + +[@react.async.component] +let make = (~selectedId: option(int), ~isEditing: bool) => { + switch (selectedId) { + | None when isEditing => + Lwt.return( + , + ) + | None => + Lwt.return( +
    + "🥺" + "Click a note on the left to view something!" +
    , + ) + | Some(id) => + let+ note: result(Note.t, string) = DB.fetchNote(id); + + switch (note) { + | Ok(note) when !isEditing => + | Ok(note) => + + | Error(error) => +
    +
    + "❌" + "There's an error while loading a single note" + error +
    +
    + }; + }; +}; diff --git a/demo/server/pages/NoteList.re b/demo/server/pages/NoteList.re new file mode 100644 index 000000000..65e2c9399 --- /dev/null +++ b/demo/server/pages/NoteList.re @@ -0,0 +1,28 @@ +open Lwt.Syntax; + +[@react.async.component] +let make = () => { + let+ notes = DB.readNotes(); + + switch (notes) { + | Error(error) => +
    + "❌" + "Couldn't read notes file" + error +
    + | Ok(notes) when notes->List.length == 0 => +
    + "There's no notes created yet!" +
    + | Ok(notes) => +
      + {notes + |> List.map((note: Note.t) => +
    • + ) + |> React.list} +
    + }; +}; diff --git a/demo/server/pages/RouterRSC.re b/demo/server/pages/RouterRSC.re new file mode 100644 index 000000000..74c36ae07 --- /dev/null +++ b/demo/server/pages/RouterRSC.re @@ -0,0 +1,170 @@ +let markdownStyles = (~background, ~text) => { + {| +.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 |} + ++ background + ++ {|; + padding-left: 1rem; + margin: 1.5rem 0; + font-style: italic; +} + +.markdown pre { + padding: 1rem; + margin: 1.5rem 0; + background-color: |} + ++ background + ++ {|; + color: |} + ++ text + ++ {|; + border-radius: 0.375rem; +} + +.markdown code { + display: block; + margin: 1rem; + padding-left: 1rem; + padding-right: 1rem; + font-family: monospace; + background-color: |} + ++ background + ++ {|; + color: |} + ++ text + ++ {|; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} +|}; +}; + +module App = { + [@react.async.component] + let make = (~selectedId, ~isEditing, ~searchText) => { + Lwt.return( + +