Skip to content
← Public packages

@kentcdodds/package-app-kit

Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.

FollowFork

starter-tanstack/app/ssr.tsx

93 lines · 2.9 KB · TypeScript
import { createMemoryHistory, RouterProvider } from '@tanstack/react-router'
import { renderToReadableStream } from 'react-dom/server.edge'
import { packageContext } from 'kody:runtime'
import { formatAboutPageData } from 'kody:@kentcdodds/package-app-kit/about'
import { readRecordedVersion } from '../src/record-version.ts'
import { listNotes } from './data/notes.ts'
import { createAppRouter, type RouterContext } from './routeTree.tsx'
import { ErrorBanner } from './ui/error-banner.tsx'
import { Document } from './ui/document.tsx'
import { toErrorMessage } from './ui/to-error-message.ts'

async function buildContext(runningSha?: string): Promise<RouterContext> {
	const version = await readRecordedVersion()
	return {
		appBasePath: packageContext?.appBasePath ?? '',
		assetBasePath: packageContext?.assetBasePath ?? '',
		clientModuleUrl: packageContext?.clientModuleUrl ?? null,
		runningSha: runningSha ?? version.sha,
		listNotes,
		async loadAbout() {
			const recorded = await readRecordedVersion()
			const about = formatAboutPageData({
				version: recorded,
				runningSha: recorded.sha,
				codeUrl: null,
			})
			return {
				sha: about.sha,
				shortSha: about.shortSha,
				message: about.message,
				publishedAt: about.publishedAt,
				committedAt: about.committedAt,
				codeUrl: about.codeUrl,
				runningSha: about.runningSha || about.sha,
			}
		},
	}
}

/**
 * SSR the matched TanStack route into a full HTML document Response.
 * Uses react-dom/server.edge renderToReadableStream (Workers-safe) — not TanStack Start / Vite.
 */
export async function renderApp(request: Request): Promise<Response> {
	const url = new URL(request.url)
	const context = await buildContext()
	const basepath = context.appBasePath.replace(/\/+$/, '') || undefined
	const history = createMemoryHistory({
		initialEntries: [url.pathname + url.search],
	})
	const router = createAppRouter({ history, context, basepath })

	try {
		await router.load()
		const stream = await renderToReadableStream(
			<RouterProvider router={router} />,
			{
				onError(error) {
					console.error('SSR render error:', error)
				},
			},
		)
		return new Response(stream, {
			headers: {
				'content-type': 'text/html; charset=utf-8',
				'cache-control': 'no-store',
			},
		})
	} catch (error) {
		const message = toErrorMessage(error)
		console.error('SSR render failed:', error)
		const stream = await renderToReadableStream(
			<Document
				title={`Error · __APP_TITLE__`}
				appBasePath={context.appBasePath}
				assetBasePath={context.assetBasePath}
				clientModuleUrl={context.clientModuleUrl}
				runningSha={context.runningSha}
			>
				<div className="pak-wrap pak-stack">
					<ErrorBanner title="Something went wrong" message={message} detail={message} />
				</div>
			</Document>,
		)
		return new Response(stream, {
			status: 500,
			headers: {
				'content-type': 'text/html; charset=utf-8',
				'cache-control': 'no-store',
			},
		})
	}
}