rewrite from monolithic hono jsx to react 19 spa with tanstack router + hono json api backend. add scan, review, execute, nodes, and setup pages. multi-stage dockerfile (node for vite build, bun for runtime). previously, server/ and src/shared/lib/ were silently excluded by global gitignore patterns (/server/ from emacs, lib/ from python). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
10 KiB
10 KiB
AGENTS.md — PWA Stack Reference
This file is the authoritative guide for AI agents (Claude Code, Cursor, Copilot, etc.) working on any app in this repository. Always follow this stack. Never introduce dependencies outside of it without explicit approval.
Stack Overview
| Layer | Tool | Version |
|---|---|---|
| Runtime | Bun | latest |
| Build | Vite | latest |
| UI Framework | React | 19+ |
| Component Library | shadcn/ui + Tailwind CSS v4 | latest |
| Routing | TanStack Router | latest |
| State Management | Zustand | latest |
| Local Database | PGlite (PostgreSQL in WASM) | latest |
| Forms | TanStack Form + Zod | latest |
| Animations | CSS View Transitions API | native |
| PWA | vite-plugin-pwa + Workbox | latest |
| Testing (unit) | Vitest + Testing Library | latest |
| Testing (e2e) | Playwright | latest |
| Code Quality | Biome + lint-staged + simple-git-hooks | latest |
Project Structure
src/
├── features/ # One folder per domain feature
│ ├── auth/
│ │ ├── components/ # UI components for this feature
│ │ ├── hooks/ # Custom hooks
│ │ ├── store.ts # Zustand store (if needed)
│ │ ├── schema.ts # Zod schemas for this feature
│ │ └── index.ts # Public API of the feature
│ └── [feature-name]/
│ └── ...
├── shared/
│ ├── components/ # Generic reusable UI (Button, Modal, etc.)
│ ├── hooks/ # Generic hooks (useMediaQuery, etc.)
│ ├── db/
│ │ ├── client.ts # PGlite instance (singleton)
│ │ ├── migrations/ # SQL migration files (001_init.sql, etc.)
│ │ └── schema.ts # Zod schemas mirroring DB tables
│ └── lib/ # Pure utility functions
├── routes/ # TanStack Router route files
│ ├── __root.tsx # Root layout
│ ├── index.tsx # / route
│ └── [feature]/
│ └── index.tsx
├── app.tsx # App entry, router provider
└── main.tsx # Bun/Vite entry point
public/
├── manifest.webmanifest # PWA manifest
└── icons/ # PWA icons (all sizes)
Rules by Layer
React & TypeScript
- Use React 19 features: use(), Suspense, transitions.
- All files are
.tsxor.ts. No.jsor.jsx. - Prefer named exports over default exports (except route files — TanStack Router requires default exports).
- No
any. Useunknownand narrow with Zod when type is uncertain. - Use
satisfiesoperator for type-safe object literals.
Styling (Tailwind + shadcn/ui)
- Never write custom CSS files. Use Tailwind utility classes exclusively.
- Use
cn()(fromsrc/shared/lib/utils.ts) for conditional class merging. - shadcn/ui components live in
src/shared/components/ui/— never modify them directly. Wrap them instead. - Dark mode via Tailwind's
dark:variant. Theclassstrategy is used (notmedia). - Responsive design: mobile-first.
sm:,md:,lg:for larger screens. - For "native app" feel on mobile: use
touch-action: manipulationon interactive elements, remove tap highlight with[-webkit-tap-highlight-color:transparent].
Routing (TanStack Router)
- Use file-based routing via
@tanstack/router-plugin/vite. - Route files live in
src/routes/. Each file exports aRoutecreated withcreateFileRoute. - Always use
LinkanduseNavigatefrom TanStack Router. Never<a href>for internal navigation. - Search params are typed via Zod — define
validateSearchon every route that uses search params. - Loaders (
loaderoption on route) are the correct place for data fetching that should block navigation.
// Example route with typed search params
export const Route = createFileRoute("/runs/")({
validateSearch: z.object({
page: z.number().default(1),
filter: z.enum(["all", "recent"]).default("all"),
}),
component: RunsPage,
});
Database (PGlite)
- The PGlite instance is a singleton exported from
src/shared/db/client.ts. - Never import PGlite directly in components. Always go through a hook or a feature's data layer.
- Use numbered SQL migration files:
src/shared/db/migrations/001_init.sql,002_add_segments.sql, etc. Run them in order on app start. - Zod schemas in
src/shared/db/schema.tsmirror every DB table. Use them to validate data coming out of PGlite. - For reactive UI updates from DB changes, use a thin wrapper with Zustand's
subscribeWithSelectoror re-query on relevant user actions.
// src/shared/db/client.ts
import { PGlite } from "@electric-sql/pglite";
let _db: PGlite | null = null;
export async function getDb(): Promise<PGlite> {
if (!_db) {
_db = new PGlite("idb://myapp");
await runMigrations(_db);
}
return _db;
}
State Management (Zustand)
- Zustand is for ephemeral UI state only: modals, active tabs, current user session, UI preferences.
- Persistent data lives in PGlite, not Zustand. Do not use
zustand/middlewarepersist for app data. - One store file per feature:
src/features/auth/store.ts. - Use
subscribeWithSelectormiddleware when components need to subscribe to slices. - Keep stores flat. Avoid deeply nested state.
// Example store
import { create } from "zustand";
interface UIStore {
activeTab: string;
setActiveTab: (tab: string) => void;
}
export const useUIStore = create<UIStore>((set) => ({
activeTab: "home",
setActiveTab: (tab) => set({ activeTab: tab }),
}));
Forms (TanStack Form + Zod)
- All forms use
useFormfrom@tanstack/react-form. - Validation is always done with a Zod schema via the
validatorsoption. - Reuse Zod schemas from
src/shared/db/schema.tsorsrc/features/[x]/schema.ts— do not duplicate validation logic. - Error messages are shown inline below the field, never in a toast.
const form = useForm({
defaultValues: { name: "" },
validators: { onChange: myZodSchema },
onSubmit: async ({ value }) => {
/* ... */
},
});
Animations (CSS View Transitions API)
- Use the native View Transitions API for page transitions. No animation libraries.
- Wrap navigation calls with
document.startViewTransition(). - TanStack Router's
RouterDevtoolsand upcoming native support handle this — check the TanStack Router docs for the current recommended integration. - Use
view-transition-nameCSS property on elements that should animate between routes. - Provide a
@media (prefers-reduced-motion: reduce)fallback that disables transitions.
/* Disable transitions for users who prefer it */
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
}
PWA (vite-plugin-pwa)
- Config lives in
vite.config.tsunder theVitePWA()plugin. - Strategy:
generateSW(notinjectManifest) unless custom SW logic is needed. - Always precache the PGlite WASM files — without this the app won't work offline.
manifest.webmanifestmust include:name,short_name,start_url: "/",display: "standalone",theme_color, icons at 192×192 and 512×512.- Register the SW with
registerType: 'autoUpdate'and show a "New version available" toast.
Testing
Unit/Component Tests (Vitest)
- Test files co-located with source:
src/features/auth/auth.test.ts. - Use Testing Library for component tests. Query by role, label, or text — never by class or id.
- Mock PGlite with an in-memory instance (no
idb://prefix) in tests. - Run with:
bun test
E2E Tests (Playwright)
- Test files in
e2e/. - Focus on critical user flows: onboarding, data entry, offline behavior, install prompt.
- Use
page.evaluate()to inspect IndexedDB/PGlite state when needed. - Run with:
bun e2e
Code Quality (Biome)
- Biome handles both formatting and linting — no Prettier, no ESLint.
- Config in
biome.jsonat project root. lint-staged+simple-git-hooksrun Biome on staged files before every commit.- CI also runs
biome check --apply— commits that fail Biome are rejected. - Never disable Biome rules with inline comments without a code comment explaining why.
Commands
bun install # Install dependencies
bun dev # Start dev server
bun build # Production build
bun preview # Preview production build locally
bun test # Run Vitest unit tests
bun e2e # Run Playwright E2E tests
bun lint # Run Biome linter
bun format # Run Biome formatter
Do Not
- ❌ Do not use
npmoryarn— always usebun - ❌ Do not add ESLint, Prettier, or Husky — Biome + simple-git-hooks covers this
- ❌ Do not use
react-queryorswr— data comes from PGlite, not a remote API - ❌ Do not store persistent app data in Zustand — use PGlite
- ❌ Do not use Framer Motion / React Spring — use CSS View Transitions
- ❌ Do not use
<a href>for internal links — use TanStack Router's<Link> - ❌ Do not write raw CSS files — use Tailwind utilities
- ❌ Do not modify files in
src/shared/components/ui/— wrap shadcn components instead - ❌ Do not introduce a new dependency without checking if the existing stack already covers the need
Adding a New Feature Checklist
- Create
src/features/[name]/folder - Add Zod schema in
src/features/[name]/schema.ts - Add DB migration in
src/shared/db/migrations/if needed - Add route file in
src/routes/[name]/index.tsx - Add Zustand store in
src/features/[name]/store.tsif UI state is needed - Write unit tests co-located with the feature
- Write at least one Playwright E2E test for the happy path
- Run
bun lintandbun testbefore committing