From 674c6f3796d688cd0877f5c36a69084c2a1b06d3 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:49:35 +0200 Subject: [PATCH] chore: add CLAUDE.md --- .coderabbit.yaml | 16 +++ CLAUDE.md | 249 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 CLAUDE.md diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 01aaa1b..7024881 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -14,6 +14,14 @@ reviews: suggested_labels: false assess_linked_issues: false path_instructions: + - path: '**/*' + instructions: | + Read CLAUDE.md at the repo root first. It defines our architecture, + testing approach (AAA pattern, plain-language behavior descriptions, + test every .ts logic file, skip pure components), component-should-be-dumb + rule (logic in hooks), folder placement rules, env-var policy (all vars + in .env, accessed via ConfigService), dependency-justification policy, + and code quality principles. Enforce every rule in CLAUDE.md during review. - path: '**/*.ts,**/*.tsx' instructions: | Review the TypeScript/React code for: @@ -22,12 +30,20 @@ reviews: - Type safety improvements - Clean code principles and readability - Security vulnerabilities + - Components holding logic that should live in a hook (see CLAUDE.md "Components must be dumb") + - Direct `import.meta.env` usage outside ConfigService + - Hardcoded user-facing strings (must go through i18n) + - New dependencies without justification in the PR description - path: '**/*.test.ts,**/*.test.tsx' instructions: | Review tests for: - Missing edge case coverage - Proper assertion usage - Test isolation and mocking correctness + - AAA structure (Arrange / Act / Assert) as defined in CLAUDE.md + - Descriptions written in plain behavior language ("When X, then Y"), + without variable/function/type names + - Tests that verify purpose/behavior, not implementation details auto_review: enabled: true drafts: false diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b13fc84 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,249 @@ +# CLAUDE.md + +> Guidance for AI assistants (Claude Code, CodeRabbit, etc.) working in this repo. Read this **before** scanning the codebase. Follow it strictly. + +This file is the source of truth for **how we work** in `mail-web`. CodeRabbit also reads it during PR reviews, so any rule here applies to both code generation and code review. + +--- + +## Communication style + +- **Get straight to the point.** No filler, no recaps of what the user just said. +- Lead with the answer or the action, then (only if needed) a short justification. +- Prefer short, direct sentences over long explanations. +- Don't summarize diffs at the end of a response — the user can read them. + +--- + +## Project at a glance + +- **Stack:** React 19 + TypeScript, Vite, Redux Toolkit (+ RTK Query), TailwindCSS v4, Vitest, react-router v7, i18next, Tiptap, IndexedDB (via `idb`). +- **Node:** `>= 24`. Package manager: **npm only** (enforced by `scripts/check-package-manager.js`). +- **Internal SDKs:** `@internxt/sdk`, `@internxt/ui`, `@internxt/lib`, `@internxt/css-config`, `internxt-crypto`. +- **Path alias:** `@/*` → `src/*` (configured in `vite.config.ts` and `vitest.config.ts`). + +### Scripts + +| Command | Purpose | +| --------------------------- | -------------------------------- | +| `npm run dev` | Vite dev server on port 3001 | +| `npm run build` | Type-check + production build | +| `npm run lint` / `lint:fix` | ESLint | +| `npm run format` | Prettier | +| `npm test` | Vitest (run once) | +| `npm run test:ui` | Vitest UI | +| `npm run test:coverage` | Coverage report (istanbul, lcov) | + +--- + +## Folder layout (where things go) + +**Don't scan the whole tree to decide placement.** Use this map. + +``` +src/ +├── assets/ # Static files (images, fonts, svgs) +├── components/ # Shared, reusable UI components (grouped by feature: compose-message/, preferences/, Sidenav/, …) +├── context/ # React contexts (dialog-manager, theme) +├── errors/ # Custom Error classes, grouped by domain (config, database, mail, oauth, storage, shared, navigation) +├── features/ # Feature modules (identity-setup, mail, welcome). Each is self-contained. +├── hooks/ # Custom React hooks, grouped by domain (auth, mail, navigation, preferences) +├── i18n/ # Translations (locales/), provider, service, types +├── routes/ # Route definitions (paths/), guards/, layouts/ +├── services/ # Singletons + API clients (config, database, sdk, oauth, local-storage, error, notifications, navigation, date, search, user) +├── store/ # Redux store: slices/, api/ (RTK Query), hooks.ts, index.ts +├── test-utils/ # setup.ts, fixtures.ts, createTestStore.ts +├── types/ # Shared TS types, grouped by domain (config, mail, oauth, preferences) +├── utils/ # Pure utility functions, one folder per util +├── App.tsx +├── main.tsx +└── constants.ts # App-wide constants (URLs, limits, defaults) +``` + +### Placement rules + +- **New feature** (a page/flow with multiple components) → [src/features/](src/features/)`/`, with `index.tsx` entry and a `components/` subfolder. +- **Reusable component** used by multiple features → [src/components/](src/components/)`/`. +- **Logic used only by one component** → a hook inside the feature's `hooks/` subfolder, or a local `use.ts` next to the component. +- **Logic reused across features** → [src/hooks/](src/hooks/)`/use.ts`. +- **API call / external integration / singleton** → [src/services/](src/services/)`/`. +- **Redux slice** → [src/store/slices/](src/store/slices/)`/`. Thunks go in a `thunks/` subfolder, one file per thunk. +- **RTK Query endpoint** → [src/store/api/](src/store/api/)`/`. +- **Custom error class** → [src/errors/](src/errors/)`/index.ts`, then re-export from [src/errors/index.ts](src/errors/index.ts). +- **Shared types** → [src/types/](src/types/)`/index.ts`. Types used in a single file stay inline. +- **Pure utility** → [src/utils/](src/utils/)`/index.ts` + co-located `.test.ts`. +- **Constant** → [src/constants.ts](src/constants.ts) if app-wide. Domain constants go inside the domain folder. +- **Route** → add a `RouteConfig` entry in [src/routes/paths/index.ts](src/routes/paths/index.ts). + +--- + +## Architecture rules + +### Components must be dumb + +Components render. Hooks think. **Always extract logic into a hook** unless the component has essentially no logic. + +- State, effects, event handlers, data fetching → live in a `use` hook. +- The component receives data + callbacks from the hook and renders JSX. +- This separation makes the logic testable without rendering the DOM and keeps components easy to refactor. + +```tsx +// Good +const useUpdateEmail = ({ onNext }) => { + const [username, setUsername] = useState(''); + const [domain, setDomain] = useState('@inxt.me'); + const handleConfirm = () => onNext(`${username}${domain}`); + return { username, setUsername, domain, setDomain, handleConfirm }; +}; + +export const UpdateEmail = (props) => { + const { username, setUsername, domain, setDomain, handleConfirm } = useUpdateEmail(props); + return (/* JSX only */); +}; +``` + +### Services are singletons + +Follow the existing pattern: + +```ts +export class FooService { + public static readonly instance: FooService = new FooService(); + // methods... +} +``` + +Use `FooService.instance.method()` from callers. This is what [MailService](src/services/sdk/mail/index.ts), [ConfigService](src/services/config/index.ts), etc. do. + +### State management + +- **Server state** → RTK Query in [src/store/api/](src/store/api/). +- **Client/session state** → Redux slices in [src/store/slices/](src/store/slices/). +- **Local UI state** → `useState` inside the hook (not the component). +- **Cross-tree UI state** → React Context in [src/context/](src/context/) (dialogs, theme). +- Always use the typed hooks: `useAppDispatch`, `useAppSelector` from [src/store/hooks.ts](src/store/hooks.ts). + +### Environment variables + +- **All env vars go in `.env`.** No hardcoded secrets or URLs that differ between environments. +- Prefix with `VITE_` so Vite exposes them to the client. +- Always access them through [ConfigService](src/services/config/index.ts) (`ConfigService.instance.getVariable('KEY')`), never `import.meta.env` directly outside that service. +- When adding a new variable: + 1. Add it to [.env.example](.env.example). + 2. Add its key to the `ConfigKeys` type in [src/types/config/](src/types/config/). + 3. Add the mapping in `configKeys` in [src/services/config/index.ts](src/services/config/index.ts). + +### Errors + +- Throw typed errors from [src/errors/](src/errors/) (see `VariableNotFoundError`, `UserNotFoundError` as templates). +- User-facing errors go through `ErrorService.instance.notifyUser(...)` or `notificationsService.show(...)`. +- Translate error messages via the i18n `translate('errors..')` pattern. + +--- + +## Testing rules + +We test to catch regressions in **behavior**, not to check that code was written. + +### Scope + +- **Test every `.ts` file that contains logic.** Services, hooks, utils, slices, thunks, RTK Query endpoints, guards — all required. +- **Don't test components** (`.tsx` with only rendering). If a component has testable logic, that logic should already be in a hook — test the hook. +- Test files are co-located: `foo.ts` → `foo.test.ts` (or `.test.tsx` for hooks using `renderHook`). + +### Structure: AAA (Arrange, Act, Assert) + +Every test follows three blank-line-separated sections: + +```ts +test('When the user is not authenticated, then it should redirect to welcome', async () => { + // Arrange + const store = createTestStore({ user: { isAuthenticated: false } }); + const navigateSpy = vi.spyOn(NavigationService.instance, 'navigate'); + + // Act + renderHook(() => useAuthGuard(), { wrapper: createWrapper(store) }); + + // Assert + expect(navigateSpy).toHaveBeenCalledWith({ id: AppView.Welcome }); +}); +``` + +### Naming & descriptions + +Describe **behavior in plain language**, not implementation. + +- Pattern: `When , then `. +- **No variable names, function names, or types** in the description. If a variable is renamed, the test description should still read correctly. +- Bad: `'returns true when isUserAuthed flag is set'` +- Good: `'When the user is authenticated, then it should allow access'` +- Use nested `describe` blocks for grouping by scenario/feature (see [useAuth.test.tsx](src/hooks/auth/useAuth.test.tsx)). + +### What to test + +- Test the **purpose** of the code: "this function exists to do X — does it do X under normal and edge conditions?" +- Cover: happy path, realistic edge cases, error/rejection paths, boundary values. +- **Don't** test TypeScript types, private fields, or internal structure. If you can't observe it from outside, it's not worth testing. +- **Don't** write tests that just mirror the implementation ("function calls X then Y then Z") — those break on every refactor without catching bugs. + +### Tools + +- **Vitest** with `globals: true`, `jsdom` environment, `TZ: UTC`. +- **Setup:** [src/test-utils/setup.ts](src/test-utils/setup.ts). +- **Fixtures:** [src/test-utils/fixtures.ts](src/test-utils/fixtures.ts) (`getMockedUser`, `getMockedTier`, etc., built with `@faker-js/faker`). +- **Store for tests:** `createTestStore` from [src/test-utils/createTestStore.ts](src/test-utils/createTestStore.ts). +- **Hooks:** `renderHook` + `act` from `@testing-library/react`. +- **IndexedDB:** `fake-indexeddb/auto`. +- Reset mocks in `beforeEach` with `vi.restoreAllMocks()`. + +--- + +## Adding dependencies + +Default answer: **don't add one**. Justify it first. + +Before proposing a new package, answer in the PR/commit: + +1. **What problem does it solve** that the existing stack (React, Redux Toolkit, Tailwind, `@internxt/ui`, Tiptap, dayjs, axios, idb, i18next) can't? +2. **Can we write it ourselves in ≤ ~100 LOC** with comparable quality? If yes, prefer that — one less supply-chain risk, one less thing to update. +3. **Maintenance status:** recent releases, open issues, TypeScript support, bundle size. +4. **License:** must be compatible with our project. + +Add it only when the cost of writing/maintaining the equivalent ourselves is clearly higher than the cost of the dependency. "It's simpler with a library" is not enough on its own — needs real complexity to justify it. + +--- + +## Code quality principles + +- **Scalable, but not over-engineered.** Build what solves today's problem cleanly. Don't design for hypothetical future requirements. +- **Easy to maintain and refactor.** Prefer clear names, small functions, narrow responsibilities. +- **No premature abstractions.** Three similar lines are fine — extract on the third repetition with real divergence, not the second. +- **No speculative error handling.** Validate at system boundaries (user input, external APIs). Trust internal code. +- **Don't touch code you don't need to change.** No drive-by refactors, no "while I'm here" cleanups. Keep diffs focused. +- **Comments only where logic isn't self-evident.** Don't restate the code in words. JSDoc on service public methods (see [MailService](src/services/sdk/mail/index.ts)) is fine. +- **i18n always.** No hardcoded user-facing strings — use `translate(...)` with keys in [src/i18n/locales/](src/i18n/locales/). + +--- + +## Before finishing a task + +1. `npm run lint` clean. +2. `npm test` passes — including new tests for any `.ts` logic added. +3. New env vars documented in `.env.example`. +4. No unused imports, no dead code, no `console.log` left over. +5. i18n keys added in **all locales** (`en`, `es`, `fr`, `it`) if you added a user-facing string. + +--- + +## For CodeRabbit specifically + +When reviewing PRs, enforce the rules above. In particular: + +- Flag components that hold non-trivial logic (should be in a hook). +- Flag `.ts` logic files without corresponding `.test.ts`. +- Flag tests whose descriptions reference variable/function/type names instead of behavior. +- Flag tests that don't follow AAA structure. +- Flag new dependencies without justification in the PR description. +- Flag direct `import.meta.env` usage outside `ConfigService`. +- Flag hardcoded user-facing strings (should go through i18n). +- Flag missing translations (new key in `en.json` but not in the other locales).