xtablo-source/.planning/codebase/CONVENTIONS.md
2026-05-14 16:01:31 +02:00

8.3 KiB

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 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 lintturbo lint → each package runs biome check .
  • pnpm lint:fixbiome check --write .
  • pnpm formatbiome 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:

// 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:

["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 levelProtectedRoute 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)