diff --git a/coldspa/renderers/React.cfm b/coldspa/renderers/React.cfm index 7259171..f19614a 100644 --- a/coldspa/renderers/React.cfm +++ b/coldspa/renderers/React.cfm @@ -6,6 +6,44 @@ React = { "name": "React", "clientEntry": "./coldspa/vite/clients/react-client.js", + + /** + * Server-renders the component to HTML by calling the SSR sidecar. + * Returns { html, css, error } -- same shape as Vue.ssrRender so + * Island.cfm can treat all renderers uniformly. + */ + "ssrRender": function(componentGlobKey, props, slotHtml, namedSlots) { + var ssrUrl = (application.coldspaConfig.ssrUrl ?: "http://127.0.0.1:5174") + & "/render/react"; + var payload = serializeJSON({ + "componentPath": arguments.componentGlobKey, + "props": arguments.props, + "slotHtml": arguments.slotHtml ?: "", + "namedSlots": arguments.namedSlots ?: {} + }); + try { + cfhttp(url=ssrUrl, method="POST", timeout=5, result="local.resp") { + cfhttpparam(type="header", name="content-type", value="application/json"); + cfhttpparam(type="body", value=payload); + } + if (find("200", local.resp.statusCode)) { + var parsed = deserializeJSON(local.resp.fileContent); + return { + "html": parsed.html ?: "", + "css": parsed.css ?: "", + "error": "" + }; + } + return { + "html": "", + "css": "", + "error": "SSR sidecar returned " & local.resp.statusCode & ": " & left(local.resp.fileContent, 500) + }; + } catch (any e) { + return { "html": "", "css": "", "error": "SSR call failed: " & e.message }; + } + }, + "render": function(mountId, componentGlobKey, propsJson, resolvedClientEntry, optionsJson) { var jsId = arguments.mountId.replace('-', '_', 'all'); return { diff --git a/coldspa/vite/clients/react-ssr.js b/coldspa/vite/clients/react-ssr.js new file mode 100644 index 0000000..fd7c6d7 --- /dev/null +++ b/coldspa/vite/clients/react-ssr.js @@ -0,0 +1,42 @@ +// Coldspa React SSR entry. Bundled by Vite with build.ssr=true via the coldspa plugin. +// __COLDSPA_GLOB__ is replaced at build time. +// +// Mirrors the client's slot semantics: +// - Default slot HTML -> as `children` +// - Each named slot -> as a same-named prop +// +// Output uses renderToString (not renderToStaticMarkup) so React inserts the +// data-reactroot / hydration markers needed for hydrateRoot on the client. +import React from 'react'; +import { renderToString } from 'react-dom/server'; + +const components = import.meta.glob('__COLDSPA_GLOB__'); + +export async function render(componentPath, props, slotHtml, namedSlots) { + const loader = components[componentPath]; + if (!loader) { + throw new Error(`[Coldspa SSR] No React component registered for path "${componentPath}".`); + } + const mod = await loader(); + + let children; + if (slotHtml) { + children = React.createElement('span', { + dangerouslySetInnerHTML: { __html: slotHtml } + }); + } + + const slotProps = {}; + if (namedSlots) { + for (const [name, html] of Object.entries(namedSlots)) { + if (html) { + slotProps[name] = React.createElement('span', { + dangerouslySetInnerHTML: { __html: html } + }); + } + } + } + + const node = React.createElement(mod.default, { ...props, ...slotProps }, children); + return renderToString(node); +} diff --git a/coldspa/vite/plugin.js b/coldspa/vite/plugin.js index 29bd22e..e6ec12e 100644 --- a/coldspa/vite/plugin.js +++ b/coldspa/vite/plugin.js @@ -41,9 +41,10 @@ const FRAMEWORK_CLIENTS = { react: toViteId(resolve(__dirname, 'clients/react-client.js')) }; -// SSR entries (only Vue is implemented today; React SSR is a TODO). +// SSR entries -- one per framework that supports server rendering. const FRAMEWORK_SSR = { - vue: toViteId(resolve(__dirname, 'clients/vue-ssr.js')) + vue: toViteId(resolve(__dirname, 'clients/vue-ssr.js')), + react: toViteId(resolve(__dirname, 'clients/react-ssr.js')) }; const DEFAULT_GLOBS = { diff --git a/coldspa/vite/ssr-server.js b/coldspa/vite/ssr-server.js index c9bd2db..4bca49c 100644 --- a/coldspa/vite/ssr-server.js +++ b/coldspa/vite/ssr-server.js @@ -34,10 +34,12 @@ const IS_PROD = process.env.NODE_ENV === 'production'; // Map :framework path segment -> SSR module entry path / id const SSR_ENTRIES_DEV = { - vue: '/coldspa/vite/clients/vue-ssr.js' + vue: '/coldspa/vite/clients/vue-ssr.js', + react: '/coldspa/vite/clients/react-ssr.js' }; const SSR_ENTRIES_PROD = { - vue: resolve(PROJECT_ROOT, 'dist-ssr', 'vue-ssr.js') + vue: resolve(PROJECT_ROOT, 'dist-ssr', 'vue-ssr.js'), + react: resolve(PROJECT_ROOT, 'dist-ssr', 'react-ssr.js') }; // --- module loading -----------------------------------------------------