forte-cli 0.3.22

CLI for the Forte fullstack web framework
import { renderToReadableStream } from "react-dom/server.browser";
import { renderToString } from "react-dom/server";
import { routes } from "./routes.generated";
import {
  serializeHookCache,
  clearHookCache,
  getCollectedCookies,
  clearCollectedCookies,
  setRequestCookie,
} from "./forte-react";
import { Head } from "../src/app";

const isDev = (import.meta as any).env?.DEV ?? false;

const clientScriptSrc = isDev ? "/.forte/client.tsx" : "/public/client.js";

const viteDevBlock = isDev
  ? `<script type="module" src="/@vite/client"></script>
<script type="module">
import RefreshRuntime from "/@react-refresh";
RefreshRuntime.injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
window.__vite_plugin_react_preamble_installed__ = true;
</script>`
  : "";

function matchRoute(
  pathname: string
): { route: (typeof routes)[0]; params: Record<string, string> } | null {
  for (const route of routes) {
    const routeParts = route.path.split("/");
    const pathParts = pathname.split("/");
    if (routeParts.length !== pathParts.length) continue;
    const params: Record<string, string> = {};
    let match = true;
    for (let i = 0; i < routeParts.length; i++) {
      if (routeParts[i].startsWith(":")) {
        params[routeParts[i].slice(1)] = pathParts[i];
      } else if (routeParts[i] !== pathParts[i]) {
        match = false;
        break;
      }
    }
    if (match) return { route, params };
  }
  return null;
}

function escapeJsonForScript(json: string): string {
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
}

async function renderStream(
  url: string,
  rawProps: any,
  cookie?: string | null
): Promise<{ stream: ReadableStream<Uint8Array>; cookies: string[] }> {
  const urlObj = new URL(url, "http://localhost");
  const matched = matchRoute(urlObj.pathname);
  const encoder = new TextEncoder();

  if (!matched) {
    return {
      stream: new ReadableStream({
        start(controller) {
          controller.enqueue(encoder.encode("Not Found"));
          controller.close();
        },
      }),
      cookies: [],
    };
  }

  clearHookCache();
  clearCollectedCookies();
  setRequestCookie(cookie ?? null);

  const [pageModule, schemaModule] = await Promise.all([
    matched.route.component(),
    matched.route.schema(),
  ]);
  const props = schemaModule.PropsSchema.parse(rawProps);
  const allProps = { ...props, params: matched.params };
  const propsJson = escapeJsonForScript(JSON.stringify(allProps));

  const PageComponent = pageModule.default;
  const reactStream = await renderToReadableStream(
    <PageComponent {...(allProps as any)} />
  );
  await reactStream.allReady;

  const headHtml = renderToString(<Head />);
  const hookCacheJson = serializeHookCache();
  const cookies = getCollectedCookies();

  const htmlHead = `<!DOCTYPE html>
<html>
<head>
${headHtml}
${viteDevBlock}
</head>
<body>
<div id="root">`;

  const htmlTail = `</div>
<script>window.__FORTE_PROPS__ = ${propsJson};</script>
<script>window.__FORTE_HOOK_CACHE__ = ${hookCacheJson};</script>
<script type="module" src="${clientScriptSrc}"></script>
</body>
</html>`;

  const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
  (async () => {
    const writer = writable.getWriter();
    await writer.write(encoder.encode(htmlHead));
    const reader = reactStream.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      await writer.write(value);
    }
    await writer.write(encoder.encode(htmlTail));
    await writer.close();
  })();

  return { stream: readable, cookies };
}

(globalThis as any).handler = async function handler(
  request: Request
): Promise<Response> {
  const bodyText = await request.text();
  let rawProps;
  try {
    rawProps = JSON.parse(bodyText);
  } catch (e) {
    console.error(
      `[SSR] JSON parse error. URL: ${request.url}, Body: "${bodyText.slice(0, 500)}"`
    );
    throw e;
  }
  const cookie = request.headers.get("cookie");
  const { stream, cookies } = await renderStream(request.url, rawProps, cookie);
  const headers = new Headers({ "Content-Type": "text/html" });
  for (const c of cookies) headers.append("Set-Cookie", c);
  return new Response(stream, { headers });
};

export { renderStream };