From 8c2c1752495f4dd43585187046075d547816f27e Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Mon, 16 Jun 2025 07:37:19 +0200 Subject: [PATCH 1/4] Add reference docs. --- docs/src/content/docs/reference/sdk-router.mdx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/reference/sdk-router.mdx b/docs/src/content/docs/reference/sdk-router.mdx index fc40e9dae..5038cd458 100644 --- a/docs/src/content/docs/reference/sdk-router.mdx +++ b/docs/src/content/docs/reference/sdk-router.mdx @@ -48,7 +48,14 @@ This will match `/user/login` and `/user/logout` ## render The `render` function is used to statically render the contents of a JSX element. It cannot contain any dynamic content. Use this to control the output of your HTML. -The `rscPayload` option is used to toggle the RSC payload that's appended to the Document. Disabling this will mean that interactivity can no longer work. Your document should not include any client side initialization. + +### Options + +The `render` function accepts an optional third parameter with the following options: + +- **`rscPayload`** (boolean, default: `true`) - Toggle the RSC payload that's appended to the Document. Disabling this will mean that interactivity can no longer work. Your document should not include any client side initialization. + +- **`ssr`** (boolean, default: `true`) - Enable or disable server-side rendering for these routes. When disabled, only client-side rendering is used, which requires `rscPayload` to be enabled. With SSR disabled, the server returns a minimal HTML shell with the RSC payload, allowing the client to hydrate the page. ```tsx import { render } from "rwsdk/router"; @@ -58,9 +65,16 @@ import { StaticDocument } from "@/app/Document"; import { routes as appRoutes } from "@/app/pages/app/routes"; import { routes as docsRoutes } from "@/app/pages/docs/routes"; +import { routes as spaRoutes } from "@/app/pages/spa/routes"; export default defineApp([ + // Default: SSR enabled with RSC payload render(ReactDocument, [prefix("/app", appRoutes)]), + + // Static rendering: SSR enabled, RSC payload disabled render(StaticDocument, [prefix("/docs", docsRoutes)], { rscPayload: false }), + + // Client-side only: SSR disabled, RSC payload enabled + render(ReactDocument, [prefix("/spa", spaRoutes)], { ssr: false }), ]); ``` From 5659f642b595614fd29c813aa07c234ca87f7552 Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Mon, 16 Jun 2025 07:45:36 +0200 Subject: [PATCH 2/4] Add SSR flag to render method. --- sdk/src/runtime/lib/router.ts | 47 +++++++++++++++++++++++++++-------- sdk/src/runtime/worker.tsx | 43 ++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/sdk/src/runtime/lib/router.ts b/sdk/src/runtime/lib/router.ts index 4e758083b..559738bbd 100644 --- a/sdk/src/runtime/lib/router.ts +++ b/sdk/src/runtime/lib/router.ts @@ -15,6 +15,7 @@ export type RwContext = { nonce: string; Document: React.FC>; rscPayload: boolean; + ssr: boolean; layouts?: React.FC>[]; }; @@ -27,7 +28,9 @@ export type RouteMiddleware = ( | Promise | Promise; -type RouteFunction = (requestInfo: T) => Response | Promise; +type RouteFunction = ( + requestInfo: T, +) => Response | Promise; type MaybePromise = T | Promise; @@ -40,7 +43,10 @@ type RouteHandler = | RouteComponent | [...RouteMiddleware[], RouteFunction | RouteComponent]; -export type Route = RouteMiddleware | RouteDefinition | Array>; +export type Route = + | RouteMiddleware + | RouteDefinition + | Array>; export type RouteDefinition = { path: string; @@ -123,7 +129,9 @@ export function matchPath( return params; } -function flattenRoutes(routes: Route[]): (RouteMiddleware | RouteDefinition)[] { +function flattenRoutes( + routes: Route[], +): (RouteMiddleware | RouteDefinition)[] { return routes.reduce((acc: Route[], route) => { if (Array.isArray(route)) { return [...acc, ...flattenRoutes(route)]; @@ -132,7 +140,9 @@ function flattenRoutes(routes: Route[]): }, []) as (RouteMiddleware | RouteDefinition)[]; } -export function defineRoutes(routes: Route[]): { +export function defineRoutes( + routes: Route[], +): { routes: Route[]; handle: ({ request, @@ -230,7 +240,10 @@ export function defineRoutes(routes: Route< }; } -export function route(path: string, handler: RouteHandler): RouteDefinition { +export function route( + path: string, + handler: RouteHandler, +): RouteDefinition { if (!path.endsWith("/")) { path = path + "/"; } @@ -241,11 +254,16 @@ export function route(path: string, handler }; } -export function index(handler: RouteHandler): RouteDefinition { +export function index( + handler: RouteHandler, +): RouteDefinition { return route("/", handler); } -export function prefix(prefixPath: string, routes: Route[]): Route[] { +export function prefix( + prefixPath: string, + routes: Route[], +): Route[] { return routes.map((r) => { if (typeof r === "function") { // Pass through middleware as-is @@ -274,7 +292,10 @@ function wrapWithLayouts( } // Check if the final route component is a client component - const isRouteClientComponent = Object.prototype.hasOwnProperty.call(Component, "$$isClientReference"); + const isRouteClientComponent = Object.prototype.hasOwnProperty.call( + Component, + "$$isClientReference", + ); // Create nested layout structure - layouts[0] should be outermost, so use reduceRight return layouts.reduceRight((WrappedComponent, Layout) => { @@ -285,7 +306,10 @@ function wrapWithLayouts( ); return React.createElement(Layout, { - children: React.createElement(WrappedComponent, isRouteClientComponent ? {} : props), + children: React.createElement( + WrappedComponent, + isRouteClientComponent ? {} : props, + ), // Only pass requestInfo to server components to avoid serialization issues ...(isClientComponent ? {} : { requestInfo }), }); @@ -322,14 +346,17 @@ export function render( /** * @param options - Configuration options for rendering. * @param options.rscPayload - Toggle the RSC payload that's appended to the Document. Disabling this will mean that interactivity no longer works. + * @param options.ssr - Disable sever side rendering for all these routes. This only allow client side rendering`, which requires `rscPayload` to be enabled. */ options: { rscPayload: boolean; - } = { rscPayload: true }, + ssr: boolean; + } = { rscPayload: true, ssr: true }, ): Route[] { const documentMiddleware: RouteMiddleware = ({ rw }) => { rw.Document = Document; rw.rscPayload = options.rscPayload; + rw.ssr = options.ssr; }; return [documentMiddleware, ...routes]; diff --git a/sdk/src/runtime/worker.tsx b/sdk/src/runtime/worker.tsx index 3afc9ee4f..093f7e727 100644 --- a/sdk/src/runtime/worker.tsx +++ b/sdk/src/runtime/worker.tsx @@ -23,7 +23,11 @@ declare global { DB: D1Database; }; } -export const defineApp = >(routes: Route[]) => { +export const defineApp = < + T extends RequestInfo = RequestInfo, +>( + routes: Route[], +) => { return { fetch: async (request: Request, env: Env, cf: ExecutionContext) => { globalThis.__webpack_require__ = ssrWebpackRequire; @@ -60,6 +64,7 @@ export const defineApp = = { @@ -144,16 +149,34 @@ export const defineApp = ; + + if (rw.ssr) { + // SSR enabled: convert RSC payload to HTML + html = await transformRscToHtmlStream({ + stream: rscPayloadStream1, + Document: rw.Document, + requestInfo: requestInfo, + }); + + if (rw.rscPayload) { + html = html.pipeThrough( + injectRSCPayload(rscPayloadStream2, { + nonce: rw.nonce, + }), + ); + } + } else { + // SSR disabled: return only RSC payload as script in minimal HTML shell + const emptyStream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("")); + controller.close(); + }, + }); - let html: ReadableStream = htmlStream; - if (rw.rscPayload) { - html = htmlStream.pipeThrough( - injectRSCPayload(rscPayloadStream2, { + html = emptyStream.pipeThrough( + injectRSCPayload(rscPayloadStream1, { nonce: rw.nonce, }), ); From f41c497fea7cae3695365e0410492f696597bcee Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Mon, 16 Jun 2025 08:58:33 +0200 Subject: [PATCH 3/4] Create a "null" fragment. --- sdk/src/runtime/worker.tsx | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/sdk/src/runtime/worker.tsx b/sdk/src/runtime/worker.tsx index 093f7e727..eca625238 100644 --- a/sdk/src/runtime/worker.tsx +++ b/sdk/src/runtime/worker.tsx @@ -152,31 +152,27 @@ export const defineApp = < let html: ReadableStream; if (rw.ssr) { - // SSR enabled: convert RSC payload to HTML html = await transformRscToHtmlStream({ stream: rscPayloadStream1, Document: rw.Document, requestInfo: requestInfo, }); - - if (rw.rscPayload) { - html = html.pipeThrough( - injectRSCPayload(rscPayloadStream2, { - nonce: rw.nonce, - }), - ); - } } else { - // SSR disabled: return only RSC payload as script in minimal HTML shell - const emptyStream = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode("")); - controller.close(); - }, + const emptyRscStream = renderToRscStream({ + node: React.createElement(React.Fragment, null, null), + actionResult: undefined, + onError, + }); + html = await transformRscToHtmlStream({ + stream: emptyRscStream, + Document: rw.Document, + requestInfo: requestInfo, }); + } - html = emptyStream.pipeThrough( - injectRSCPayload(rscPayloadStream1, { + if (rw.rscPayload) { + html = html.pipeThrough( + injectRSCPayload(rscPayloadStream2, { nonce: rw.nonce, }), ); From ac8a605d20fa56ce0916940c0f90f777b3039d60 Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Mon, 16 Jun 2025 13:18:15 +0200 Subject: [PATCH 4/4] Fix defaults. --- sdk/src/runtime/lib/router.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sdk/src/runtime/lib/router.ts b/sdk/src/runtime/lib/router.ts index 559738bbd..d94c22dbe 100644 --- a/sdk/src/runtime/lib/router.ts +++ b/sdk/src/runtime/lib/router.ts @@ -349,14 +349,20 @@ export function render( * @param options.ssr - Disable sever side rendering for all these routes. This only allow client side rendering`, which requires `rscPayload` to be enabled. */ options: { - rscPayload: boolean; - ssr: boolean; - } = { rscPayload: true, ssr: true }, + rscPayload?: boolean; + ssr?: boolean; + } = {}, ): Route[] { + options = { + rscPayload: true, + ssr: true, + ...options, + }; + const documentMiddleware: RouteMiddleware = ({ rw }) => { rw.Document = Document; - rw.rscPayload = options.rscPayload; - rw.ssr = options.ssr; + rw.rscPayload = options.rscPayload ?? true; + rw.ssr = options.ssr ?? true; }; return [documentMiddleware, ...routes];