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, "