import Cache from "graphql-react/Cache.mjs";import CacheContext from "graphql-react/CacheContext.mjs";import Loading from "graphql-react/Loading.mjs";import LoadingContext from "graphql-react/LoadingContext.mjs";import { createElement as h, Fragment } from "react";import { renderToString } from "react-dom/server";import waterfallRender from "react-waterfall-render/waterfallRender.mjs";import { Status, STATUS_TEXT } from "std/http/http_status.ts";import { serve as serveHttp } from "std/http/server.ts";import { toFileUrl } from "std/path/mod.ts";
import HeadManager from "./HeadManager.mjs";import HeadManagerContext from "./HeadManagerContext.mjs";import Html from "./Html.mjs";import publicFileResponse from "./publicFileResponse.mjs";import readImportMapFile from "./readImportMapFile.mjs";import RouteContext from "./RouteContext.mjs";import TransferContext from "./TransferContext.mjs";
export default async function serve({ clientImportMap, esModuleShimsSrc = "https://unpkg.com/es-module-shims", publicDir = new URL("public/", toFileUrl(Deno.cwd() + "/")), htmlComponent = Html, port, signal,}) { if (!(clientImportMap instanceof URL)) { throw new TypeError("Option `clientImportMap` must be a `URL` instance."); }
if (typeof esModuleShimsSrc !== "string") { throw new TypeError("Option `esModuleShimsSrc` must be a string."); }
if (!(publicDir instanceof URL)) { throw new TypeError("Option `publicDir` must be a `URL` instance."); }
if (!publicDir.href.endsWith("/")) { throw new TypeError("Option `publicDir` must be a URL ending with `/`."); }
if (typeof htmlComponent !== "function") { throw new TypeError("Option `htmlComponent` must be a function."); }
if (typeof port !== "number") { throw new TypeError("Option `port` must be a number."); }
if (signal !== undefined && !(signal instanceof AbortSignal)) { throw new TypeError("Option `signal` must be an `AbortSignal` instance."); }
const clientImportMapContent = await readImportMapFile(clientImportMap); const routerFileUrl = new URL("router.mjs", publicDir);
let router;
try { ({ default: router } = await import(routerFileUrl.href)); } catch (cause) { throw new Error(`Error importing \`${routerFileUrl.href}\`.`, { cause }); }
const appFileUrl = new URL("components/App.mjs", publicDir);
let App;
try { ({ default: App } = await import(appFileUrl.href)); } catch (cause) { throw new Error(`Error importing \`${appFileUrl.href}\`.`, { cause }); }
const close = serveHttp( async (request) => { const routeUrl = new URL(request.url);
const headerXForwardedProto = request.headers.get("x-forwarded-proto"); if (headerXForwardedProto) { routeUrl.protocol = headerXForwardedProto + ":"; }
const headerXForwardedHost = request.headers.get("x-forwarded-host"); if (headerXForwardedHost) { routeUrl.hostname = headerXForwardedHost; }
if ( routeUrl.pathname !== "/" ) { try { return await publicFileResponse(request, publicDir); } catch (cause) { if (!(cause instanceof Deno.errors.NotFound)) { throw new Error("Ruck couldn’t serve a public file.", { cause }); } } }
const headManager = new HeadManager();
let routePlan;
try { routePlan = router(routeUrl, headManager, true); } catch (cause) { throw new Error( `Ruck couldn’t plan the route for URL ${routeUrl.href}.`, { cause }, ); }
if (typeof routePlan !== "object" || !routePlan) { throw new TypeError( `Ruck route plan is invalid for URL ${routeUrl.href}.`, ); }
let routeContent;
try { routeContent = await routePlan.content; } catch (cause) { throw new Error( `Ruck couldn’t resolve the route content for URL ${routeUrl.href}.`, { cause }, ); }
try { const cache = new Cache(); const loading = new Loading();
const responseInit = { status: Status.OK, statusText: STATUS_TEXT.get(Status.OK), headers: new Headers({ "content-type": "text/html; charset=utf-8", }), };
const transfer = { request, responseInit };
const bodyReactRootInnerHtml = await waterfallRender( h( TransferContext.Provider, { value: transfer }, h( RouteContext.Provider, { value: { url: routeUrl, content: routeContent, }, }, h( HeadManagerContext.Provider, { value: headManager }, h( CacheContext.Provider, { value: cache }, h(LoadingContext.Provider, { value: loading }, h(App)), ), ), ), ), renderToString, );
const responseBody = `<!DOCTYPE html>${ renderToString( h( TransferContext.Provider, { value: transfer }, h(htmlComponent, { esModuleShimsScript: h("script", { async: true, src: esModuleShimsSrc, }), importMapScript: h("script", { type: "importmap", dangerouslySetInnerHTML: { __html: JSON.stringify(clientImportMapContent), }, }), headReactRoot: h( Fragment, null, h("meta", { name: "ruck-head-start" }), headManager.getHeadContent(), h("meta", { name: "ruck-head-end" }), ), bodyReactRoot: h("div", { id: "ruck-app", dangerouslySetInnerHTML: { __html: bodyReactRootInnerHtml }, }), hydrationScript: h("script", { type: "module", dangerouslySetInnerHTML: { __html: `import hydrate from "ruck/hydrate.mjs";import App from "/components/App.mjs";import router from "/router.mjs";
hydrate({ router, appComponent: App, cacheData: ${JSON.stringify(cache.store)},});`, }, }), }), ), ) }`;
return new Response(responseBody, responseInit); } catch (cause) { throw new Error("Ruck couldn’t serve the rendered route.", { cause }); } }, { port, signal }, );
return { close };}