UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

199 lines (198 loc) 10.1 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { normalizeActionResult } from "./render/normalizeActionResult"; import { renderDocumentHtmlStream } from "./render/renderDocumentHtmlStream"; import { renderToRscStream } from "./render/renderToRscStream"; import { injectRSCPayload } from "rsc-html-stream/server"; import { ErrorResponse } from "./error"; import { rscActionHandler } from "./register/worker"; import { getRequestInfo, runWithRequestInfo, runWithRequestInfoOverrides, } from "./requestInfo/worker"; import { ssrWebpackRequire } from "./imports/worker"; import { defineRoutes } from "./lib/router"; import { generateNonce } from "./lib/utils"; export * from "./requestInfo/types"; export const defineApp = (routes) => { return { fetch: async (request, env, cf) => { globalThis.__webpack_require__ = ssrWebpackRequire; const router = defineRoutes(routes); // context(justinvdm, 5 Feb 2025): Serve assets requests using the assets service binding // todo(justinvdm, 5 Feb 2025): Find a way to avoid this so asset requests are served directly // rather than first needing to go through the worker if (request.url.includes("/assets/")) { const url = new URL(request.url); url.pathname = url.pathname.slice("/assets/".length); return env.ASSETS.fetch(new Request(url.toString(), request)); } else if (import.meta.env.VITE_IS_DEV_SERVER && new URL(request.url).pathname === "/__worker-run") { const url = new URL(request.url); const scriptPath = url.searchParams.get("script"); if (!scriptPath) { return new Response("Missing 'script' query parameter", { status: 400, }); } try { const scriptModule = await import(/* @vite-ignore */ scriptPath); if (scriptModule.default) { await scriptModule.default(request, env, cf); } return new Response("Script executed successfully"); } catch (e) { console.error(`Error executing script: ${scriptPath}\n\n${e.stack}`); return new Response(`Error executing script: ${e.message}`, { status: 500, }); } } else if (import.meta.env.VITE_IS_DEV_SERVER && request.url.includes("/__vite_preamble__")) { return new Response('import RefreshRuntime from "/@react-refresh"; RefreshRuntime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true;', { headers: { "content-type": "text/javascript", }, }); } try { const url = new URL(request.url); const isRSCRequest = url.searchParams.has("__rsc") || request.headers.get("accept")?.includes("text/x-component"); const isAction = url.searchParams.has("__rsc_action_id"); const rw = { Document: DefaultDocument, nonce: generateNonce(), rscPayload: true, ssr: true, databases: new Map(), scriptsToBeLoaded: new Set(), entryScripts: new Set(), inlineScripts: new Set(), pageRouteResolved: undefined, }; const userResponseInit = { status: 200, headers: new Headers(), }; const outerRequestInfo = { request, cf, params: {}, ctx: {}, rw, response: userResponseInit, isAction, }; const createPageElement = (requestInfo, Page) => { let pageElement; if (isClientReference(Page)) { const { ctx, params } = requestInfo; // context(justinvdm, 25 Feb 2025): If the page is a client reference, we need to avoid passing // down props the client shouldn't get (e.g. env). For safety, we pick the allowed props explicitly. pageElement = _jsx(Page, { ctx: ctx, params: params }); } else { pageElement = _jsx(Page, { ...requestInfo }); } return pageElement; }; const renderPage = async (requestInfo, Page, onError) => { if (isClientReference(requestInfo.rw.Document)) { if (import.meta.env.DEV) { console.error("Document cannot be a client component"); } return new Response(null, { status: 500, }); } const actionResult = normalizeActionResult(requestInfo.rw.actionResult); const pageElement = createPageElement(requestInfo, Page); const { rscPayload: shouldInjectRSCPayload } = rw; let rscPayloadStream = renderToRscStream({ input: { node: pageElement, actionResult, }, onError, }); if (isRSCRequest) { const responseHeaders = new Headers(userResponseInit.headers); responseHeaders.set("content-type", "text/x-component; charset=utf-8"); return new Response(rscPayloadStream, { status: userResponseInit.status, statusText: userResponseInit.statusText, headers: responseHeaders, }); } let injectRSCPayloadStream; if (shouldInjectRSCPayload) { const [rscPayloadStream1, rscPayloadStream2] = rscPayloadStream.tee(); rscPayloadStream = rscPayloadStream1; injectRSCPayloadStream = injectRSCPayload(rscPayloadStream2, { nonce: rw.nonce, }); } let html = await renderDocumentHtmlStream({ rscPayloadStream: rscPayloadStream, Document: rw.Document, requestInfo: requestInfo, onError, shouldSSR: rw.ssr, }); if (injectRSCPayloadStream) { html = html.pipeThrough(injectRSCPayloadStream); } const responseHeaders = new Headers(userResponseInit.headers); responseHeaders.set("content-type", "text/html; charset=utf-8"); return new Response(html, { status: userResponseInit.status, statusText: userResponseInit.statusText, headers: responseHeaders, }); }; const response = await runWithRequestInfo(outerRequestInfo, async () => new Promise(async (resolve, reject) => { try { resolve(await router.handle({ request, renderPage, getRequestInfo: getRequestInfo, runWithRequestInfoOverrides, onError: reject, rscActionHandler, })); } catch (e) { reject(e); } })); // context(justinvdm, 18 Mar 2025): In some cases, such as a .fetch() call to a durable object instance, or Response.redirect(), // we need to return a mutable response object. const mutableResponse = new Response(response.body, response); // Merge headers from user response init (these take precedence) if (userResponseInit.headers) { const userResponseHeaders = new Headers(userResponseInit.headers); for (const [key, value] of userResponseHeaders.entries()) { if (!response.headers.has(key)) { mutableResponse.headers.set(key, value); } } } await rw.pageRouteResolved?.promise; return mutableResponse; } catch (e) { if (e instanceof ErrorResponse) { return new Response(e.message, { status: e.code }); } if (e instanceof Response) { return e; } console.error("rwsdk: Received an unhandled error:\n\n%s", e); throw e; } }, }; }; export const DefaultDocument = ({ children, }) => (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" })] }), _jsx("body", { children: _jsx("div", { id: "root", children: children }) })] })); const isClientReference = (Component) => { return Object.prototype.hasOwnProperty.call(Component, "$$isClientReference"); };