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>
254 lines
10 KiB
Markdown
254 lines
10 KiB
Markdown
# 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 `.tsx` or `.ts`. No `.js` or `.jsx`.
|
||
- Prefer named exports over default exports (except route files — TanStack Router requires default exports).
|
||
- No `any`. Use `unknown` and narrow with Zod when type is uncertain.
|
||
- Use `satisfies` operator for type-safe object literals.
|
||
|
||
### Styling (Tailwind + shadcn/ui)
|
||
|
||
- **Never write custom CSS files.** Use Tailwind utility classes exclusively.
|
||
- Use `cn()` (from `src/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. The `class` strategy is used (not `media`).
|
||
- Responsive design: mobile-first. `sm:`, `md:`, `lg:` for larger screens.
|
||
- For "native app" feel on mobile: use `touch-action: manipulation` on 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 a `Route` created with `createFileRoute`.
|
||
- **Always use `Link` and `useNavigate`** from TanStack Router. Never `<a href>` for internal navigation.
|
||
- Search params are typed via Zod — define `validateSearch` on every route that uses search params.
|
||
- Loaders (`loader` option on route) are the correct place for data fetching that should block navigation.
|
||
|
||
```ts
|
||
// 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.ts` mirror 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 `subscribeWithSelector` or re-query on relevant user actions.
|
||
|
||
```ts
|
||
// 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/middleware` persist for app data.
|
||
- One store file per feature: `src/features/auth/store.ts`.
|
||
- Use `subscribeWithSelector` middleware when components need to subscribe to slices.
|
||
- Keep stores flat. Avoid deeply nested state.
|
||
|
||
```ts
|
||
// 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 `useForm` from `@tanstack/react-form`.
|
||
- Validation is always done with a Zod schema via the `validators` option.
|
||
- **Reuse Zod schemas** from `src/shared/db/schema.ts` or `src/features/[x]/schema.ts` — do not duplicate validation logic.
|
||
- Error messages are shown inline below the field, never in a toast.
|
||
|
||
```ts
|
||
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 `RouterDevtools` and upcoming native support handle this — check the TanStack Router docs for the current recommended integration.
|
||
- Use `view-transition-name` CSS property on elements that should animate between routes.
|
||
- Provide a `@media (prefers-reduced-motion: reduce)` fallback that disables transitions.
|
||
|
||
```css
|
||
/* 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.ts` under the `VitePWA()` plugin.
|
||
- Strategy: `generateSW` (not `injectManifest`) unless custom SW logic is needed.
|
||
- **Always precache** the PGlite WASM files — without this the app won't work offline.
|
||
- `manifest.webmanifest` must 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.json` at project root.
|
||
- `lint-staged` + `simple-git-hooks` run 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
|
||
|
||
```bash
|
||
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 `npm` or `yarn` — always use `bun`
|
||
- ❌ Do not add ESLint, Prettier, or Husky — Biome + simple-git-hooks covers this
|
||
- ❌ Do not use `react-query` or `swr` — 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
|
||
|
||
1. Create `src/features/[name]/` folder
|
||
2. Add Zod schema in `src/features/[name]/schema.ts`
|
||
3. Add DB migration in `src/shared/db/migrations/` if needed
|
||
4. Add route file in `src/routes/[name]/index.tsx`
|
||
5. Add Zustand store in `src/features/[name]/store.ts` if UI state is needed
|
||
6. Write unit tests co-located with the feature
|
||
7. Write at least one Playwright E2E test for the happy path
|
||
8. Run `bun lint` and `bun test` before committing
|