161 lines
8.3 KiB
Markdown
161 lines
8.3 KiB
Markdown
# Code Conventions
|
|
|
|
**Last updated:** 2026-05-14
|
|
**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source`
|
|
**Apps:** `apps/main`, `apps/external`, `apps/api`, `apps/admin`, `apps/chat-worker`, `apps/clients`
|
|
**Packages:** `packages/shared`, `packages/ui`, `packages/shared-types`, `packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views`
|
|
|
|
## Linter & Formatter — Biome
|
|
|
|
The repo uses a single root [Biome](https://biomejs.dev) config: `biome.json` (Biome 2.2.5, pinned in root `package.json` devDependencies).
|
|
|
|
Formatting rules (from `biome.json`):
|
|
- `indentStyle: "space"`, `indentWidth: 2`
|
|
- `lineEnding: "lf"`, `lineWidth: 100`
|
|
- JS: `quoteStyle: "double"`, `jsxQuoteStyle: "double"`, `semicolons: "always"`, `trailingCommas: "es5"`, `arrowParentheses: "always"`, `bracketSpacing: true`, `bracketSameLine: false`
|
|
- JSON formatter enabled with same indent settings; parser allows comments but not trailing commas
|
|
|
|
Key lint rules turned on (Biome `recommended: false` — rules are explicit):
|
|
- `suspicious.noExplicitAny: "error"` (overridden to `off` for some files via overrides; warn in `xtablo-expo`)
|
|
- `correctness.noUnusedVariables`, `noUnusedImports`, `noUndeclaredVariables` — all `error`
|
|
- `style.useConst`, `useTemplate`, `noNamespace`, `noCommonJs` — all `error`
|
|
- `complexity.noBannedTypes`, `suspicious.noDebugger`, `suspicious.noEmptyBlockStatements` — all `error`
|
|
|
|
Per-package scripts wrap Biome:
|
|
- `pnpm lint` → `turbo lint` → each package runs `biome check .`
|
|
- `pnpm lint:fix` → `biome check --write .`
|
|
- `pnpm format` → `biome format --write .`
|
|
|
|
## TypeScript Conventions
|
|
|
|
Every package/app ships its own `tsconfig.json`; there is no root TS config.
|
|
|
|
Strictness — every config sets `strict: true`. Additional flags consistent across configs:
|
|
- `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true`
|
|
- `noUncheckedIndexedAccess: true` for `packages/shared-types`, `packages/shared`
|
|
- `isolatedModules: true`, `moduleDetection: "force"`, `skipLibCheck: true`
|
|
- API uses `verbatimModuleSyntax: true` (`apps/api/tsconfig.json`) — type-only imports must be explicit
|
|
|
|
Module systems:
|
|
- Frontend apps use `module: ESNext` + `moduleResolution: "bundler"` (e.g. `apps/main/tsconfig.app.json`)
|
|
- API uses `module: NodeNext` (TypeScript compiled output via `tsc`)
|
|
- All packages declare `"type": "module"` in package.json
|
|
|
|
Path aliases (defined per app, resolved by Vite via `vite-tsconfig-paths`):
|
|
- `apps/main`: `@ui/*` → `./src/*`, `@xtablo/auth-ui` → `../../packages/auth-ui/src`, `@xtablo/ui/*` → `../../packages/ui/src/*` (see `apps/main/tsconfig.app.json`)
|
|
- `apps/main/tsconfig.json`: also `@external/*` → `src/external/*`
|
|
|
|
Package import discipline (per `CLAUDE.md`):
|
|
- No circular dependencies between packages
|
|
- `apps/api` may only import from `@xtablo/shared-types`
|
|
- Frontend apps may import from all shared packages
|
|
- `@xtablo/shared` and `@xtablo/ui` are **source-only** — TypeScript is consumed directly via `vite-tsconfig-paths`; no build step
|
|
|
|
Types-first workflow:
|
|
- Database types are auto-generated into `packages/shared-types/src/database.types.ts` via `npx supabase gen types typescript`
|
|
- Domain types in `@xtablo/shared-types` are derived from `database.types.ts` (nulls removed, refined shapes)
|
|
- API response types live in the same package so frontends and the API agree
|
|
|
|
## React Component Conventions
|
|
|
|
- Functional components only (no class components observed)
|
|
- TSX files use named exports for components, e.g. `export function CustomModal(...)` (see `apps/main/src/components/CustomModal.tsx`)
|
|
- Co-located unit tests: `Foo.tsx` + `Foo.test.tsx`
|
|
- Naming suffix conventions (per `CLAUDE.md`):
|
|
- Dialogs / modals → `*Modal.tsx` (e.g. `CustomModal.tsx`)
|
|
- Page sections → `*Section.tsx` (e.g. `TabloEventsSection.tsx`)
|
|
- Card surfaces → `*Card.tsx` (e.g. `EventTypeCard.tsx`, `AvailabilityCard.tsx`, `EventTypeCard.test.tsx`)
|
|
- Shared primitives (Radix + Tailwind) live in `packages/ui/src`
|
|
- Cross-app business hooks/contexts live in `packages/shared/src`
|
|
|
|
## Hook Patterns
|
|
|
|
React Query (TanStack Query v5) is the primary server-state tool. Standard return shapes:
|
|
|
|
```ts
|
|
// Queries
|
|
const { data, isLoading, error } = useMyQuery();
|
|
|
|
// Mutations
|
|
const { mutate, isPending } = useMyMutation();
|
|
```
|
|
|
|
- Default cache time: 5 minutes (configured in `packages/shared`'s QueryClient)
|
|
- Mutations invalidate targeted keys explicitly rather than blowing away the whole cache
|
|
|
|
Zustand handles global client state (notably the authenticated user store):
|
|
- `useUser()` — throws if no session (use inside protected routes)
|
|
- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public surfaces)
|
|
|
|
## Query Key Conventions
|
|
|
|
Hierarchical keys, with the resource name first and identifiers cascading deeper:
|
|
|
|
```ts
|
|
["tablos"] // list
|
|
["tablos", tabloId] // single
|
|
["tablo-files", tabloId] // related collection
|
|
```
|
|
|
|
Invalidations should match the deepest key that needs to be refreshed.
|
|
|
|
## Error Handling
|
|
|
|
- User-facing: `toast.add({ ... })` (toast system in `packages/shared` / `packages/ui`) — messages should be friendly and actionable
|
|
- Technical: `console.error` for developer-only context (stack traces, raw API errors)
|
|
- API errors are caught at the React Query hook layer and surfaced via `error` from `useQuery`/`useMutation`; UI components branch on `isError`
|
|
- Server side (`apps/api`): Hono routers return `c.json({ error: ... }, statusCode)`; middleware handles auth failures with 401s (see `docs/MIDDLEWARE_TESTS.md`)
|
|
|
|
## Loading States — three levels
|
|
|
|
1. **Route level** — `ProtectedRoute` shows a full-page spinner while the session resolves
|
|
2. **Feature level** — React Query `isLoading` / `isPending` drives section-level skeletons or spinners
|
|
3. **Action level** — Buttons set `disabled` during the related mutation's `isPending`
|
|
|
|
Empty / error states are explicit branches (no silent fallbacks).
|
|
|
|
## Import / Export Patterns
|
|
|
|
- ESM throughout (`"type": "module"` in every package.json; Biome enforces `noCommonJs`)
|
|
- Source-only packages (`@xtablo/shared`, `@xtablo/ui`) export from `src/index.ts` and are consumed via TS path aliases — no `dist/` involved
|
|
- Compiled packages (`@xtablo/shared-types` and `apps/api`) emit `dist/` via `tsc`; `shared-types` includes `declaration: true` and `declarationMap: true`
|
|
- Type-only imports preferred where supported (`verbatimModuleSyntax` is on for the API)
|
|
- Biome's `noUnusedImports` rule will flag dead imports at lint time
|
|
|
|
## File Organization
|
|
|
|
```
|
|
apps/<app>/src/
|
|
components/ # UI components, *.tsx + *.test.tsx
|
|
contexts/ # React contexts (e.g. UpgradeBlockContext.tsx)
|
|
providers/ # Store / provider wrappers (e.g. UserStoreProvider.tsx)
|
|
hooks/ # App-specific hooks
|
|
pages/ or routes/ # Route entry points
|
|
lib/ # Utilities, route table, api client setup
|
|
utils/testHelpers # Render-with-providers wrappers for tests
|
|
packages/<pkg>/src/
|
|
index.ts # Single barrel export
|
|
... # Domain folders mirror app layout
|
|
```
|
|
|
|
API layout (`apps/api/src`):
|
|
- `routers/` — Hono routers grouped by concern (`public.ts`, `authRouter.ts`, `tablo.ts`, `stripe.ts`, etc.)
|
|
- `middlewares/` — Auth, Supabase, R2, Stream, email injection
|
|
- `helpers/` — Pure logic (testable in isolation, e.g. `helpers/orgIcons.ts` + `orgIcons.test.ts`)
|
|
- `__tests__/` — Test-only fixtures, setup, globalSetup, route + middleware suites
|
|
|
|
## Type Safety
|
|
|
|
- `noExplicitAny` is on as `error` in the root config (relaxed to `warn` in `xtablo-expo` and `off` for select API/legacy file overrides)
|
|
- Prefer derived types from `@xtablo/shared-types` over inline shape literals
|
|
- `Database` table types are generated; domain types should narrow them rather than redeclare from scratch
|
|
- `noUncheckedIndexedAccess: true` in shared packages — index access returns `T | undefined`; handle the undefined branch explicitly
|
|
- API enforces `verbatimModuleSyntax`, so `import type { ... }` is required for type-only imports — relevant for compiled API code
|
|
|
|
## Reference files
|
|
|
|
- `biome.json` — single source for lint + format
|
|
- `apps/main/tsconfig.app.json` — canonical frontend TS config
|
|
- `apps/api/tsconfig.json` — canonical backend TS config
|
|
- `packages/shared-types/tsconfig.json` — type-only package config
|
|
- `CLAUDE.md` — high-level conventions (this doc expands it with verified specifics)
|