← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
import {
Outlet,
createRootRouteWithContext,
createRoute,
createRouter,
useRouter,
useRouterState,
} from '@tanstack/react-router'
import type { RouterHistory } from '@tanstack/react-router'
import type { AppShellContext } from './router-context.ts'
import { AppShellReactContext } from './router-context.ts'
import { Document } from './ui/document.tsx'
import { ErrorBanner } from './ui/error-banner.tsx'
import { HomePage } from './pages/home.tsx'
import { AboutPage } from './pages/about.tsx'
import { NotesPage } from './pages/notes.tsx'
import { toErrorMessage } from './ui/to-error-message.ts'
import type { Note } from './data/notes.ts'
export type RouterContext = AppShellContext
type PageStaticData = { page?: string; title?: string }
const rootRoute = createRootRouteWithContext<RouterContext>()({
component: function Root() {
const router = useRouter()
const ctx = router.options.context as RouterContext
const leaf = useRouterState({ select: (s) => s.matches.at(-1) })
const staticData = (leaf?.staticData || {}) as PageStaticData
return (
<AppShellReactContext.Provider value={ctx}>
<Document
title={staticData.title || '__APP_TITLE__'}
page={staticData.page || ''}
runningSha={ctx.runningSha}
appBasePath={ctx.appBasePath}
assetBasePath={ctx.assetBasePath}
clientModuleUrl={ctx.clientModuleUrl}
>
<Outlet />
</Document>
</AppShellReactContext.Provider>
)
},
errorComponent: function RootError({ error }) {
const router = useRouter()
const ctx = router.options.context as RouterContext
return (
<AppShellReactContext.Provider value={ctx}>
<Document
title="Error · __APP_TITLE__"
appBasePath={ctx.appBasePath}
assetBasePath={ctx.assetBasePath}
clientModuleUrl={ctx.clientModuleUrl}
runningSha={ctx.runningSha}
>
<div className="pak-wrap pak-stack">
<ErrorBanner
title="Something went wrong"
message={toErrorMessage(error)}
detail={toErrorMessage(error)}
/>
</div>
</Document>
</AppShellReactContext.Provider>
)
},
})
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
staticData: { page: 'home', title: '__APP_TITLE__' } satisfies PageStaticData,
loader: async ({ context }): Promise<{ noteCount: number }> => {
if (context.listNotes) {
const notes = await context.listNotes()
return { noteCount: notes.length }
}
const appBase = context.appBasePath.replace(/\/$/, '')
const res = await fetch(`${appBase}/notes`, {
headers: { Accept: 'application/json' },
credentials: 'same-origin',
})
if (!res.ok) return { noteCount: 0 }
const data = (await res.json()) as { notes?: Array<Note> }
return { noteCount: Array.isArray(data.notes) ? data.notes.length : 0 }
},
component: HomePage,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
staticData: { page: 'about', title: 'About · __APP_TITLE__' } satisfies PageStaticData,
loader: async ({ context }) => {
if (context.loadAbout) return await context.loadAbout()
const appBase = context.appBasePath.replace(/\/$/, '')
const res = await fetch(`${appBase}/api/version`, {
cache: 'no-store',
credentials: 'same-origin',
})
const version = res.ok
? ((await res.json()) as {
sha?: string
shortSha?: string
message?: string
publishedAt?: string
committedAt?: string
})
: {}
return {
sha: version.sha || '',
shortSha: version.shortSha || '',
message: version.message || '',
publishedAt: version.publishedAt || '',
committedAt: version.committedAt || '',
codeUrl: null as string | null,
runningSha: version.sha || '',
}
},
component: AboutPage,
})
const notesRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/notes',
staticData: { page: 'notes', title: 'Notes · __APP_TITLE__' } satisfies PageStaticData,
loader: async ({ context }): Promise<{ notes: Array<Note> }> => {
if (context.listNotes) {
return { notes: await context.listNotes() }
}
const appBase = context.appBasePath.replace(/\/$/, '')
const res = await fetch(`${appBase}/notes`, {
headers: { Accept: 'application/json' },
credentials: 'same-origin',
})
if (!res.ok) return { notes: [] }
const data = (await res.json()) as { notes?: Array<Note> }
return { notes: Array.isArray(data.notes) ? data.notes : [] }
},
component: NotesPage,
})
export const routeTree = rootRoute.addChildren([homeRoute, aboutRoute, notesRoute])
export function createAppRouter(input: {
history: RouterHistory
context: RouterContext
basepath?: string
}) {
return createRouter({
routeTree,
history: input.history,
context: input.context,
basepath: input.basepath && input.basepath !== '/' ? input.basepath : undefined,
defaultPreload: 'intent',
scrollRestoration: true,
})
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createAppRouter>
}
}