← 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)
}
},
}