summaryrefslogtreecommitdiff
path: root/bs5/universal/js
diff options
context:
space:
mode:
Diffstat (limited to 'bs5/universal/js')
-rw-r--r--bs5/universal/js/ClientRouter.re22
-rw-r--r--bs5/universal/js/Dream.re1
-rw-r--r--bs5/universal/js/ReactServerDOMEsbuild.js282
-rw-r--r--bs5/universal/js/ReactServerDOMEsbuild.re72
-rw-r--r--bs5/universal/js/ReactServerDOMWebpack.re78
-rw-r--r--bs5/universal/js/dune27
6 files changed, 482 insertions, 0 deletions
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"))