← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
starter-fetch/src/app.ts
220 lines · 5.8 KB · TypeScriptimport { packageContext } from 'kody:runtime'
import { buildWebManifest, renderServiceWorker } from 'kody:@kentcdodds/package-app-kit/sw'
import { readRecordedVersion } from './record-version.ts'
import { defaultManifestIcons, iconPngResponse } from 'kody:@kentcdodds/package-app-kit/icons'
import { serverCachified } from 'kody:@kentcdodds/package-app-kit/server-cache'
import {
clientBootResponse,
clientModuleResponse,
} from 'kody:@kentcdodds/package-app-kit/client'
import { renderAboutPage, renderAppPage } from './app/page.ts'
import { addNote, deleteNote, listNotes } from './app/notes.ts'
const CACHE_NAME = '__CACHE_NAME__'
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
}
/**
* Dual-path client:
* - Prefer packageContext.clientModuleUrl (this app's kody.app.client bundle).
* - Interim fallback on older platforms: kit committed/bundled ESM via ./client.
* Never import ./client/index.ts from the Worker (separate graphs).
*/
function interimClientResponse(path: string): Response | null {
const ctx = packageContext as AppPackageContext | null
if (ctx?.clientModuleUrl) 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 appHealth() {
let fresh = false
const value = await serverCachified({
key: 'starter-health',
ttl: 15_000,
getFreshValue: async () => {
fresh = true
return {
ok: true,
generatedAt: new Date().toISOString(),
cache: 'cachified+packageStorage',
}
},
})
return { ...value, fresh }
}
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([readRecordedVersion(), appHealth()])
return html(
renderAppPage({
...paths,
runningSha: version.sha,
version,
codeUrl: null,
health,
}),
)
}
if (method === 'GET' && path === '/about') {
const version = await readRecordedVersion()
return html(
renderAboutPage({
...paths,
runningSha: version.sha,
version,
codeUrl: null,
}),
)
}
if (method === 'GET') {
const clientRes = interimClientResponse(path)
if (clientRes) return clientRes
}
if (method === 'GET' && path === '/manifest.webmanifest') {
return new Response(
JSON.stringify(
buildWebManifest({
appBasePath: paths.appBasePath,
name: '__APP_TITLE__',
shortName: '__APP_TITLE__',
description: 'Scaffolded from @kentcdodds/package-app-kit starter/.',
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; prefer public/sw.js under assetBasePath when platform assets are live.
if (method === 'GET' && path === '/sw.js') {
const allow = `${paths.appBasePath.replace(/\/$/, '')}/`
const precache = [
'/',
'/about',
'/manifest.webmanifest',
'/icons/icon-192.png',
'/icons/icon-512.png',
]
if (!paths.clientModuleUrl) precache.push('/app.js', '/client/boot.js')
return new Response(
renderServiceWorker({
cacheName: CACHE_NAME,
offlineTitle: '__APP_TITLE__ offline',
precachePaths: precache,
htmlPaths: ['/', '/about'],
}),
{
headers: {
'content-type': 'text/javascript; charset=utf-8',
'cache-control': 'no-store',
'service-worker-allowed': allow,
},
},
)
}
if (method === 'GET' && path === '/api/version') {
return json(await readRecordedVersion())
}
if (method === 'GET' && path === '/api/health') {
return json(await appHealth())
}
if (method === 'GET' && path === '/api/notes') {
return json({ notes: await listNotes() })
}
if (method === 'POST' && path === '/api/notes') {
const body = (await request.json().catch(() => null)) as { title?: string } | null
const note = await addNote(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 deleteNote(id)
return json(result, result.ok ? 200 : 404)
}
return json({ ok: false, error: 'Not found' }, 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)
}
},
}