diff options
Diffstat (limited to 'bs5/universal/js')
-rw-r--r-- | bs5/universal/js/ClientRouter.re | 22 | ||||
-rw-r--r-- | bs5/universal/js/Dream.re | 1 | ||||
-rw-r--r-- | bs5/universal/js/ReactServerDOMEsbuild.js | 282 | ||||
-rw-r--r-- | bs5/universal/js/ReactServerDOMEsbuild.re | 72 | ||||
-rw-r--r-- | bs5/universal/js/ReactServerDOMWebpack.re | 78 | ||||
-rw-r--r-- | bs5/universal/js/dune | 27 |
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")) |