Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kentcdodds/lineage

src/app.ts

100 lines · 3.0 KB · TypeScript
import { packageContext } from 'kody:runtime'
import getSeries from './series.ts'
import { getRepoSettings, ingestSummary } from './storage.ts'
import { artResponse } from './ui/art.ts'
import { needsCanonicalAppRedirect, serviceWorkerAllowedPath } from './ui/app-url.ts'
import { iconSvg } from './ui/icon.ts'
import { lineageManifest } from './ui/manifest.ts'
import { renderAppPage } from './ui/page.ts'
import { serviceWorkerScript } from './ui/sw-script.ts'
import { getAppVersion } from './version.ts'

/**
 * Hosted Lineage app: PWA chart, series API, and installable shell.
 */
export default {
	async fetch(request: Request): Promise<Response> {
		const url = new URL(request.url)
		const appBasePath = packageContext?.appBasePath || '/packages/lineage'
		const hostedUrl = packageContext?.hostedUrl || `https://kentcdodds.kody.run${appBasePath}`
		if (needsCanonicalAppRedirect(url.pathname, appBasePath)) {
			url.pathname = `${appBasePath}/`
			return Response.redirect(url, 308)
		}
		const path = stripMount(url.pathname, appBasePath)

		if (path === '/manifest.webmanifest') {
			return json(lineageManifest(appBasePath), 'application/manifest+json; charset=utf-8')
		}
		const artwork = artResponse(path)
		if (artwork) return artwork
		if (path === '/icon.svg') {
			return new Response(iconSvg, {
				headers: { 'Content-Type': 'image/svg+xml; charset=utf-8' },
			})
		}
		if (path === '/sw.js') {
			return new Response(serviceWorkerScript(appBasePath), {
				headers: {
					'Content-Type': 'application/javascript; charset=utf-8',
					'Cache-Control': 'no-cache',
					'Service-Worker-Allowed': serviceWorkerAllowedPath(appBasePath),
				},
			})
		}
		if (path === '/api/series') {
			const series = await getSeries({
				from: url.searchParams.get('from'),
				to: url.searchParams.get('to'),
				mode:
					(url.searchParams.get('mode') as
						| 'cumulative'
						| 'delta'
						| 'net'
						| null) ?? 'cumulative',
			})
			return json(series)
		}
		if (path === '/api/status') {
			return json({
				repo: await getRepoSettings(),
				ingest: await ingestSummary(),
			})
		}
		if (path === '/api/version') {
			return json(await getAppVersion())
		}

		const [repo, version] = await Promise.all([getRepoSettings(), getAppVersion()])
		return new Response(
			renderAppPage({
				appBasePath,
				hostedUrl,
				repoLabel: `${repo.owner}/${repo.repo}`,
				version,
			}),
			{
				headers: {
					'Content-Type': 'text/html; charset=utf-8',
					'Cache-Control': 'no-store',
				},
			},
		)
	},
}

function stripMount(pathname: string, appBasePath: string): string {
	let rest = pathname
	if (pathname === appBasePath || pathname.startsWith(`${appBasePath}/`)) {
		rest = pathname.slice(appBasePath.length) || '/'
	}
	if (!rest.startsWith('/')) rest = `/${rest}`
	if (rest.length > 1 && rest.endsWith('/')) rest = rest.slice(0, -1)
	return rest || '/'
}

function json(data: unknown, contentType = 'application/json; charset=utf-8') {
	return new Response(JSON.stringify(data), {
		headers: { 'Content-Type': contentType },
	})
}