Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .cursor/rules/00-overview.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
description: Documentation map and core rules for the Languages Learner project
alwaysApply: true
---

# Languages Learner — project rules

The single source of truth is the `docs/` directory; the cross-tool entry point is `AGENTS.md`.
Edit content in `docs/`, not in the rules. Full map: [docs/index.md](../../docs/index.md).

## Always keep in mind

- **English only** in docs and code (identifiers, comments, commit messages).
- **FSD flows downward** in `apps/web/src/ui` (`app → pages → widgets → features → entities →
shared`), no cycles — [docs/architecture/package-interaction.md](../../docs/architecture/package-interaction.md).
- **`res.locals` is the SSR contract**; Supabase credentials are runtime config, **no `VITE_`
copies** — [docs/architecture/web-ssr.md](../../docs/architecture/web-ssr.md).
- **Do not containerise `apps/web`**; do not add a web service to `docker-compose.dev.yml`.
- **Do not edit generated files** (`database.types.ts`, `openapi.json`, `api.ts`).
- Formatting and code rules — [docs/conventions.md](../../docs/conventions.md).

## Definition of Done

Change code → update the matching page in `docs/`; run `pnpm docs:check` before a PR. Trigger table:
[docs/maintaining-docs.md](../../docs/maintaining-docs.md).
17 changes: 17 additions & 0 deletions .cursor/rules/backend.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
description: NestJS backend, Supabase, RLS
globs:
- apps/backend/**
---

# apps/backend — NestJS

Full description: [docs/architecture/backend.md](../../docs/architecture/backend.md).

- Supabase clients are created only in `SupabaseService`; there is no service-role key.
- **No `Scope.REQUEST`** and no authentication inside provider factories (it short-circuits the
guard chain and `ThrottlerGuard`). Covered by `test/rate-limit.e2e.test.ts`.
- Env is validated at startup (`src/config/env.validation.ts`).
- After changing a DTO / return type, regenerate the contract:
`pnpm --filter app-backend generate:api-schemas` —
[docs/architecture/api-contract.md](../../docs/architecture/api-contract.md).
16 changes: 16 additions & 0 deletions .cursor/rules/i18n.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
description: Hash-based i18n (FormatJS) and the locale pipeline
globs:
- apps/web/src/locales/**
- packages/i18n-core/**
- packages/locale/**
---

# i18n

Full description: [docs/architecture/i18n.md](../../docs/architecture/i18n.md).

- Message IDs are hashes (`formatjs/enforce-id`); **never author them by hand**.
- After changing messages: `pnpm --filter app-web i18n:extract`, then `i18n:manage` (syncs
`en.json`/`ru.json` and compiles into `src/locales/compiled/`). Commit the updated locales —
they are an input to typecheck.
15 changes: 15 additions & 0 deletions .cursor/rules/packages.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
description: Package interaction and package documentation
globs:
- packages/**
---

# packages/*

- How packages depend on each other and the layers —
[docs/architecture/package-interaction.md](../../docs/architecture/package-interaction.md).
- Each package's purpose and public API — [docs/packages/](../../docs/packages/).
- Change a package's public exports/purpose → update `docs/packages/<dir>.md`. Scripts live in the
package's `README.md` (docs link to them, they don't duplicate them).
- Add/remove a package → add/remove its page in `docs/packages/` (otherwise `pnpm docs:check`
fails) and fix the links in [docs/index.md](../../docs/index.md).
19 changes: 19 additions & 0 deletions .cursor/rules/web-ssr.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
description: Custom SSR of apps/web and the res.locals contract
globs:
- apps/web/src/server/**
- apps/web/src/ui/app/entries/**
- apps/web/src/shared/**
- apps/web/src/vite-env.d.ts
---

# apps/web — SSR

Full description: [docs/architecture/web-ssr.md](../../docs/architecture/web-ssr.md).

- `res.locals` is the SSR contract. Adding server-derived state touches the middleware, `render`,
the serialization into `window.CLIENT`, the `Locals` type (`src/server/typings.d.ts`), and
`Window.CLIENT` (`src/vite-env.d.ts`).
- Supabase credentials are runtime config via `window.CLIENT`; **do not add `VITE_` copies**.
- The `/api/*` proxy is registered before Vite's middlewares.
- `apps/web` is deliberately not containerised.
2 changes: 1 addition & 1 deletion .github/workflows/components-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Check if any project with test:component is affected
id: check-affected
run: |
# Используем --with-target для фильтрации затронутых проектов по наличию target
# Use --with-target to filter affected projects by the presence of the target
AFFECTED_WITH_TARGET=$(pnpm nx show projects --affected --with-target test:component --json)
HAS_TARGET=$(echo "$AFFECTED_WITH_TARGET" | jq -r 'if type == "array" then (length > 0) else false end')
echo "has-affected-projects=$HAS_TARGET" >> $GITHUB_OUTPUT
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/precommit-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,21 @@ jobs:

- name: Check dependency versions
run: pnpm run deps:check

check-docs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm

- name: Install dependencies
uses: ./.github/actions/install-dependencies

- name: Check docs coverage and links
run: pnpm run docs:check
# # TODO: is all har sanitized?
# # TODO: is i18n extracted?
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AGENTS.md

Entry point for AI agents (Cursor reads this file natively; Claude Code imports it from
`CLAUDE.md`). The rules and architecture live in [`docs/`](./docs/) — that is the single source of
truth; this file is only a map and a short summary. Edit content in `docs/`, not here.

## What this project is

Languages Learner is an **Nx + pnpm** monorepo for a language-learning web app. `apps/*` (`web`,
`backend`, `storybook`, `web-e2e`) and `packages/*` (reusable libraries). Node 20, pnpm 10.6.2.
Full overview: [docs/architecture/overview.md](./docs/architecture/overview.md).

## Documentation map

- [docs/index.md](./docs/index.md) — the root map of all documentation.
- Architecture: [overview](./docs/architecture/overview.md) ·
[package interaction](./docs/architecture/package-interaction.md) ·
[web SSR](./docs/architecture/web-ssr.md) · [backend](./docs/architecture/backend.md) ·
[data layer](./docs/architecture/data-layer.md) · [i18n](./docs/architecture/i18n.md) ·
[API contract](./docs/architecture/api-contract.md)
- Packages: [docs/packages/](./docs/packages/) (one page per workspace package).
- [Conventions](./docs/conventions.md) · [Roadmap](./docs/roadmap.md) ·
[Maintaining docs](./docs/maintaining-docs.md)

## Core working rules

- **English only** in docs and code (identifiers, comments, commit messages). Chat with the user in
the user's language.
- **FSD flows downward.** In `apps/web/src/ui`, imports go `app → pages → widgets → features →
entities → shared`; cycles are forbidden (`madge`). See
[package-interaction.md](./docs/architecture/package-interaction.md).
- **`res.locals` is the SSR contract.** Adding server-derived state touches the middleware,
`render`, `window.CLIENT`, `Locals`, and `Window.CLIENT`. See
[web-ssr.md](./docs/architecture/web-ssr.md).
- **Supabase credentials are runtime config.** Do not add `VITE_`-prefixed copies (that bakes them
into the bundle).
- **Do not containerise `apps/web`** and do not add a web service to `docker-compose.dev.yml`.
- **Backend:** no `Scope.REQUEST` and no authentication inside provider factories; there is no
service-role key. See [backend.md](./docs/architecture/backend.md).
- **Do not edit generated files** (`database.types.ts`, `openapi.json`, `api.ts`); after changing a
DTO, regenerate the schemas. See [api-contract.md](./docs/architecture/api-contract.md).
- Formatting and code rules — [conventions.md](./docs/conventions.md).

## Keeping docs current (Definition of Done)

Change code → update the matching page in `docs/`; complete a roadmap item → move it to Done. Before
a PR: `pnpm docs:check`. The "code path → what to update" trigger table and the full rule set are in
[maintaining-docs.md](./docs/maintaining-docs.md).
134 changes: 40 additions & 94 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,99 +1,45 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Guidance for Claude Code when working in this repository.

## Repository

Nx + pnpm monorepo for **Languages Learner**, a language-learning web app. Workspaces are `apps/*` and `packages/*` (see `pnpm-workspace.yaml`). Node 20 (`.nvmrc`), pnpm 10.6.2 via Corepack.

`nx.json` is intentionally minimal — Nx infers targets from each package's `package.json` scripts. Root scripts are `nx run-many -t <target>` fan-outs, so a target only runs where the package defines it.

## Commands

Root (all workspaces):

```bash
pnpm lint # nx run-many -t lint
pnpm typecheck # nx run-many -t typecheck
pnpm test:unit # vitest watch; pnpm test:unit:ci for a single run
pnpm stylelint
pnpm circular-deps # madge, UI code only
pnpm knip # unused files/exports/deps
pnpm deps:check # syncpack — dependency versions consistent across packages
```

Single package/app — use pnpm filters with the **package name**, not the folder:

```bash
pnpm --filter app-web dev # apps/web
pnpm --filter app-backend start:dev # apps/backend
pnpm --filter @languages-learner/uikit typecheck
```

Full local stack: `pnpm dev` at the root — starts the backend container from `docker-compose.dev.yml` (detached), then runs `apps/web` natively in the foreground. `pnpm dev:logs` follows the backend, `pnpm dev:build` rebuilds its image after a dependency change, `pnpm dev:down` stops it.

**`apps/web` is deliberately not containerised.** Bind-mounted reads cross the host boundary at milliseconds per file, which Vite pays thousands of times per render, and containers get inotify events only for files in the Linux filesystem, so the watcher would have to poll. Both costs are invisible for the backend and severe for Vite. Do not "complete" the compose file by adding a web service.

Single unit test: only `packages/class-names` and `packages/error-utils` define `test:unit`. Run e.g. `pnpm --filter @languages-learner/class-names test:unit -- <name-pattern>` (vitest). Root `vitest.config.ts` only picks up `**/*.test.ts`.

Component tests (Playwright CT, uikit only) run inside Docker for stable screenshots:

```bash
pnpm --filter @languages-learner/uikit test:component:docker
pnpm --filter @languages-learner/uikit test:component:update:docker # update snapshots
```

Storybook: `pnpm --filter @languages-learner/storybook storybook` (port 6006).

## Architecture

### apps/web — custom SSR, not a framework

Two independent TypeScript projects under `src/`, each with its own `tsconfig.json` and its own build:

- `src/server/` — Express server (`src/server/main.ts`), built by `tsx`/nodemon in dev.
- `src/ui/` — React app, built by Vite twice: `vite build` (client) and `vite build --ssr src/ui/app/entries/main-server.tsx` (server bundle).

The server is the SSR orchestrator. In development it runs Vite in `middlewareMode` and loads the render function through `vite.ssrLoadModule`; in production it reads the prebuilt HTML shell and imports `dist/server/main-server.mjs`. It then string-replaces `<!--app-head-->`, `<!--app-html-->`, a theme class on `<html>`, and `window.CLIENT = {}` with `res.locals`.
This file is a **thin router**. The single source of truth is [`docs/`](./docs/); the shared,
tool-agnostic entry point is [`AGENTS.md`](./AGENTS.md). Edit rules and architecture in `docs/`,
not here — the `@`-imports below pull that content into context automatically.

**`res.locals` is the SSR contract.** Express middlewares (`src/server/middlewares/`: `supabaseConfig`, `user`, `locale`, `theme`) populate it, `render(res.locals)` consumes it, and the same object is serialized into `window.CLIENT` for client hydration. Adding server-derived state means touching all three points — plus `Locals` in `src/server/typings.d.ts`, and `Window.CLIENT` in `src/vite-env.d.ts` for anything the browser reads.

**Supabase credentials are runtime config, not build config.** `SUPABASE_PROJECT_URL` / `SUPABASE_ANON_KEY` are read from the environment by `src/shared/supabase-config.ts` and reach the browser through `window.CLIENT` — nothing is inlined into the bundle, so one image serves every environment and a rotated key applies on the next request. Do not reintroduce `VITE_`-prefixed copies: that would bake credentials into the bundle at build time and silently desynchronise the SSR server from the backend, which reads the runtime values.

`/api/*` is proxied to the NestJS backend (`BACKEND_PORT`, default 3001) — the proxy middleware must stay registered before Vite's middlewares.

### apps/web UI layers (Feature-Sliced Design)

`src/ui/` follows FSD: `app` → `pages` → `widgets` → `features` → `entities` → `shared`. Imports flow downward only; circular imports are enforced by `madge` (`circular-deps:ui`). Path aliases inside `src/ui`: `@/*` → `src/ui/*`, `@@/*` → repo root, `shared/*` → `src/shared/*` (cross-cutting, server+ui), `locales/*` → `src/locales/*`. The server project deliberately has no aliases yet (relative paths only).

### Data layer

Two coexisting systems: **TanStack Query** wrapped by `@normy/react-query`'s `QueryNormalizerProvider` for normalized cache updates, and **Gravity UI DataSource** via the shared `dataManager` from `@languages-learner/data-source` (`DataManagerContext`). Check which one a slice already uses before adding data fetching.

### apps/backend — NestJS

Standard Nest module layout (`words`, `user`, `auth`, `supabase`, `config`, `common`). `SupabaseService` is the only place clients are created: `getAuthClient()` verifies tokens, `getClientForUser(token)` returns an RLS-scoped client for the caller. There is deliberately **no service-role key** — a service-role client would bypass RLS on every request. Auth is a global `AuthGuard` (opt out with `@Public()`) that attaches `{ id, email, accessToken }`, read in handlers via `@CurrentUser()`.

**Keep this provider graph free of `Scope.REQUEST`, and never authenticate inside a provider factory.** A request-scoped Supabase client used to throw `UnauthorizedException` from its factory; because Nest resolves request-scoped providers before guards run, that throw short-circuited the guard chain and the global `ThrottlerGuard` never executed on protected routes. The 401 looked correct, so nothing surfaced it. Covered by `apps/backend/test/rate-limit.e2e.test.ts`.

Env is validated at startup (`src/config/env.validation.ts`); missing vars exit the process with a readable message instead of failing on the first request. See `apps/backend/README.md`.

Database types are generated, not hand-written: `generate-types` in `apps/backend` regenerates `database.types.ts` and copies it into `packages/api`. Do not edit those files (they are eslint-ignored, so `lint:fix` cannot reformat them). Backend `lint` is check-only; use `lint:fix` to autofix — same split as every other package.

**The API contract is generated too.** Backend DTOs → Swagger → `packages/api/src/schemas/openapi.json` → `api.ts` → SDK → `apps/web`. After changing a DTO or a handler's return type, run `pnpm --filter app-backend generate:api-schemas` (needs a valid root `.env`) and commit the regenerated schemas, otherwise `apps/web` typechecks against a stale contract.

### i18n

FormatJS + react-intl with **hash-based message IDs** — `formatjs/enforce-id` (ESLint) requires the `[sha512:contenthash:base64:6]` pattern, so IDs are generated, never authored. Workflow: `pnpm --filter app-web i18n:extract` → `src/locales/extracted.json`, then `i18n:manage` extracts, syncs `en.json`/`ru.json`, and compiles into `src/locales/compiled/` (which the app imports directly). Compiled locales are a build input for typecheck too.

## Conventions

- Prettier: 4-space indent, 100 columns, trailing commas, LF line endings (`linebreak-style` is an ESLint error on Windows too). Tailwind class sorting via `prettier-plugin-tailwindcss`.
- Type imports must be inline (`import { type Foo }` style enforced by `@typescript-eslint/consistent-type-imports` with `fixStyle: "inline-type-imports"`).
- `newline-before-return` is an error.
- PR titles must be conventional/semantic (`feat:`, `fix:`, `chore:` …) — enforced by `.github/workflows/check-pr-title.yml`.
- Each app and package has its own `README.md` listing its scripts; check it before inventing commands.

## Notes
## Repository

The public tree ships no database migrations and no turnkey local backend — API-dependent work needs your own Supabase project (see `.env.example`) or mocks.
Nx + pnpm monorepo for **Languages Learner**, a language-learning web app. Workspaces are `apps/*`
and `packages/*` (`pnpm-workspace.yaml`). Node 20 (`.nvmrc`), pnpm 10.6.2 via Corepack. `nx.json`
is intentionally minimal — Nx infers targets from each package's `package.json` scripts; root
scripts are `nx run-many -t <target>` fan-outs.

Commands, formatting and code rules live in [`docs/conventions.md`](./docs/conventions.md)
(imported below). Each app/package also has its own `README.md` listing its scripts — check it
before inventing commands. Some specifics not repeated elsewhere:

- Single unit test: only `packages/class-names` and `packages/error-utils` define `test:unit`,
e.g. `pnpm --filter @languages-learner/class-names test:unit -- <name-pattern>` (vitest). Root
`vitest.config.ts` only picks up `**/*.test.ts`.
- Component tests (Playwright CT, uikit only) run in Docker for stable screenshots:
`pnpm --filter @languages-learner/uikit test:component:docker` (append `:update` to update snaps).
- Storybook: `pnpm --filter @languages-learner/storybook storybook` (port 6006).
- The public tree ships no DB migrations and no turnkey local backend — API-dependent work needs
your own Supabase project (see `.env.example`) or mocks.

## Shared rules and architecture (source of truth)

@AGENTS.md
@docs/conventions.md
@docs/architecture/overview.md
@docs/architecture/package-interaction.md
@docs/architecture/web-ssr.md
@docs/architecture/backend.md
@docs/architecture/data-layer.md
@docs/architecture/i18n.md
@docs/architecture/api-contract.md

## Keeping docs current

Updating `docs/` is part of the Definition of Done for any code change, and `pnpm docs:check` runs
in CI. The trigger table (code path → doc to update) is in
[`docs/maintaining-docs.md`](./docs/maintaining-docs.md).
Loading
Loading