Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kentcdodds/package-app-kit

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

archive/fetch-demo/app.ts

240 lines · 6.5 KB · TypeScript
/**
 * Lean demo app entry for this kit package only.
 * Distinct from `starter-remix/` — scaffolder embeds starter-remix/, not this live demo.
 *
 * Dual-path client:
 * - Prefer `packageContext.clientModuleUrl` (kody.app.client) when non-null.
 * - Fall back to Worker-served `/client/boot.js` on older platforms (interim embed).
 * Worker must NOT import `src/client/index.ts` (separate graphs).
 */
import { packageContext } from 'kody:runtime'
import { renderAboutPage, renderDemoPage } from './demo-page.ts'
import { clientBootResponse, clientModuleResponse } from './client-assets.ts'
import { getAppVersion } from './version.ts'
import { buildWebManifest, renderServiceWorker } from './sw.ts'
import { defaultManifestIcons, iconPngResponse } from './icons.ts'
import { serverCachified } from './server-cache.ts'
import { addDemoNote, deleteDemoNote, listDemoNotes } from './demo-notes.ts'

const CACHE_NAME = 'package-app-kit-demo-v6'

type AppPackageContext = {
	hostedUrl?: string
	appBasePath?: string
	assetBasePath?: string | null
	clientModuleUrl?: string | null
}

function json(data: unknown, status = 200) {
	return new Response(JSON.stringify(data), {
		status,
		headers: {
			'content-type': 'application/json; charset=utf-8',
			'cache-control': 'no-store',
		},
	})
}

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

function appPaths() {
	const ctx = packageContext as AppPackageContext | null
	if (!ctx?.hostedUrl) {
		throw new Error('This module must run as a package app.')
	}
	return {
		hostedUrl: ctx.hostedUrl,
		appBasePath: ctx.appBasePath ?? '',
		assetBasePath: ctx.assetBasePath ?? null,
		clientModuleUrl: ctx.clientModuleUrl ?? null,
	}
}

function pathnameOf(request: Request) {
	const url = new URL(request.url)
	const path = url.pathname === '' ? '/' : url.pathname
	return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path
}

function codeUrlForKit() {
	return 'https://kody.codes/@kentcdodds/package-app-kit'
}

async function demoHealth() {
	let fresh = false
	const value = await serverCachified({
		key: 'demo-health',
		ttl: 15_000,
		getFreshValue: async () => {
			fresh = true
			return {
				ok: true,
				generatedAt: new Date().toISOString(),
				cache: 'cachified+packageStorage',
			}
		},
	})
	return { ...value, fresh }
}

/**
 * Interim fallback only: when `clientModuleUrl` is null (older platform),
 * serve committed/bundled browser ESM from `./client-assets`.
 * Documented dual-path — remove once all hosts expose clientModuleUrl.
 */
function interimClientResponse(path: string): Response | null {
	const ctx = packageContext as AppPackageContext | null
	if (ctx?.clientModuleUrl) {
		// Platform owns `/_assets/client.<hash>.js`; do not shadow it from the Worker.
		return null
	}
	if (path === '/app.js' || path === '/client/boot.js') return clientBootResponse()
	if (path.startsWith('/client/')) {
		return clientModuleResponse(path.slice('/client/'.length))
	}
	return null
}

async function handleRequest(request: Request): Promise<Response> {
	const path = pathnameOf(request)
	const method = request.method.toUpperCase()
	const paths = appPaths()

	if (method === 'GET' && (path === '/' || path === '')) {
		const [version, health] = await Promise.all([getAppVersion(), demoHealth()])
		return html(
			renderDemoPage({
				...paths,
				runningSha: version.sha,
				version,
				codeUrl: codeUrlForKit(),
				health,
			}),
		)
	}

	if (method === 'GET' && path === '/about') {
		const version = await getAppVersion()
		return html(
			renderAboutPage({
				...paths,
				runningSha: version.sha,
				version,
				codeUrl: codeUrlForKit(),
			}),
		)
	}

	if (method === 'GET') {
		const interim = interimClientResponse(path)
		if (interim) return interim
	}

	if (method === 'GET' && path === '/manifest.webmanifest') {
		return new Response(
			JSON.stringify(
				buildWebManifest({
					appBasePath: paths.appBasePath,
					name: 'Package App Kit',
					shortName: 'App Kit',
					description:
						'Demo of design tokens, install, About, and update toast from package-app-kit.',
					icons: defaultManifestIcons(paths.appBasePath),
				}),
			),
			{
				headers: {
					'content-type': 'application/manifest+json; charset=utf-8',
					'cache-control': 'no-store',
				},
			},
		)
	}

	if (method === 'GET' && path === '/icons/icon-192.png') {
		return iconPngResponse(192)
	}

	if (method === 'GET' && path === '/icons/icon-512.png') {
		return iconPngResponse(512)
	}

	// Interim Worker SW when assets dir is not served by the platform yet.
	// Prefer public/sw.js via packageContext.assetBasePath when #2284 is live.
	if (method === 'GET' && path === '/sw.js') {
		const allow = `${paths.appBasePath.replace(/\/$/, '')}/`
		const assetBase = (paths.assetBasePath || '').replace(/\/$/, '')
		const precache = [
			'/',
			'/about',
			'/manifest.webmanifest',
			'/icons/icon-192.png',
			'/icons/icon-512.png',
		]
		if (!paths.clientModuleUrl) {
			precache.push('/client/boot.js', '/app.js')
		}
		return new Response(
			renderServiceWorker({
				cacheName: CACHE_NAME,
				offlineTitle: 'Kit demo offline',
				precachePaths: precache,
				htmlPaths: ['/', '/about'],
			}),
			{
				headers: {
					'content-type': 'text/javascript; charset=utf-8',
					'cache-control': 'no-store',
					'service-worker-allowed': allow,
					...(assetBase
						? { 'x-pak-prefer-assets-sw': `${assetBase}/sw.js` }
						: {}),
				},
			},
		)
	}

	if (method === 'GET' && path === '/api/version') {
		return json(await getAppVersion())
	}

	if (method === 'GET' && path === '/api/health') {
		return json(await demoHealth())
	}

	if (method === 'GET' && path === '/api/notes') {
		return json({ notes: await listDemoNotes() })
	}

	if (method === 'POST' && path === '/api/notes') {
		const body = (await request.json().catch(() => null)) as { title?: string } | null
		const note = await addDemoNote(String(body?.title || ''))
		return json({ note }, 201)
	}

	if (method === 'DELETE' && path.startsWith('/api/notes/')) {
		const id = decodeURIComponent(path.slice('/api/notes/'.length))
		const result = await deleteDemoNote(id)
		return json(result, result.ok ? 200 : 404)
	}

	return json({ ok: false, error: 'Not found', path }, 404)
}

export default {
	async fetch(request: Request): Promise<Response> {
		try {
			return await handleRequest(request)
		} catch (error) {
			const message = error instanceof Error ? error.message : String(error)
			return json({ ok: false, error: message }, 500)
		}
	},
}