Skip to content

app/router.ts

63 lines · 2.7 KB · TypeScript
Raw
import { packageContext } from 'kody:runtime'
import { renderAbout } from './about.ts'
import { ASSETS } from './assets.ts'
import { BUILD } from './build-info.ts'
import { renderGame } from './game.ts'
import { manifest, serviceWorker } from './pwa.ts'
import type { PageCtx } from './ui.ts'

const HOST = /^[a-z0-9.-]+(:\d{1,5})?$/i

/** Public origin for absolute og:image URLs. The runtime may rewrite request.url, so prefer the forwarded/Host header. */
function publicOrigin(request: Request, url: URL) {
	const raw = (request.headers.get('x-forwarded-host') || request.headers.get('host') || url.host).split(',')[0]!.trim()
	if (!HOST.test(raw)) return 'https://asteroids.kentcdodds.com'
	const local = /^(localhost|127\.|\[::1\])/.test(raw)
	return `${local ? 'http' : 'https'}://${raw.toLowerCase()}`
}

function pageCtx(request: Request, url: URL): PageCtx {
	const appBase = String(packageContext?.appBasePath ?? '').replace(/\/+$/, '')
	// Images are served by this router from embedded bytes under {appBase}/a/.
	return { origin: publicOrigin(request, url), appBase, assetBase: `${appBase}/a` }
}

function asset(path: string) {
	const a = ASSETS[path]
	if (!a) return null
	const bytes = Uint8Array.from(atob(a.b64), (c) => c.charCodeAt(0))
	return new Response(bytes, { headers: { 'content-type': a.type, 'cache-control': 'public, max-age=3600' } })
}

const html = (body: string) =>
	new Response(body, { headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' } })

export default {
	fetch(request: Request) {
		const url = new URL(request.url)
		const ctx = pageCtx(request, url)
		let path = url.pathname
		if (ctx.appBase && path.startsWith(ctx.appBase)) path = path.slice(ctx.appBase.length)
		path = path.replace(/\/+$/, '') || '/'
		if (path === '/health') return Response.json({ ok: true, service: 'asteroids', build: BUILD.buildId })
		if (path === '/version.json') return Response.json(BUILD, { headers: { 'cache-control': 'no-store' } })
		if (path === '/manifest.webmanifest') {
			return new Response(JSON.stringify(manifest(ctx)), {
				headers: { 'content-type': 'application/manifest+json', 'cache-control': 'no-cache' },
			})
		}
		if (path === '/sw.js') {
			return new Response(serviceWorker(ctx), {
				headers: {
					'content-type': 'text/javascript; charset=utf-8',
					'cache-control': 'no-cache',
					'service-worker-allowed': `${ctx.appBase}/`,
				},
			})
		}
		if (path.startsWith('/a/')) { const r = asset(path.slice(2)); if (r) return r }
		if (path === '/about') return html(renderAbout(ctx))
		if (path === '/') return html(renderGame(ctx))
		return new Response('Not found', { status: 404, headers: { 'content-type': 'text/plain' } })
	},
}