- Fix MD040 (11 bare fences): add language tags (text/bash) across 7 files - Fix MD031 (2 violations): add blank lines around fence in GETTING-STARTED.md - Wire 'Markdown lint' step to fast-checks job (after Format check, before Typecheck) - Reformat .markdownlint-cli2.jsonc per Prettier (trailing commas in JSONC) - pnpm md:lint exits 0; pnpm format:check exits 0; gate can fail on bare fence (verified)
270 lines
13 KiB
Markdown
270 lines
13 KiB
Markdown
<!-- generated-by: gsd-doc-writer -->
|
|
|
|
# Development Guide
|
|
|
|
Local development setup and workflows for FamilySync — a pnpm monorepo with two workspaces: `apps/api` (Hono + Node.js) and `apps/pwa` (React + Vite).
|
|
|
|
## Repo Layout
|
|
|
|
```text
|
|
familysync/
|
|
├── apps/
|
|
│ ├── api/ # Hono API server — Node.js 22, TypeScript, Drizzle/MariaDB
|
|
│ └── pwa/ # React 19 PWA — Vite, TanStack Query, Schedule-X
|
|
├── docker-compose.yml
|
|
├── docker-compose.dev.yml
|
|
├── package.json # Root workspace scripts
|
|
└── pnpm-workspace.yaml
|
|
```
|
|
|
|
Key paths inside `apps/api/src/`:
|
|
|
|
```text
|
|
src/
|
|
├── db/
|
|
│ ├── schema.ts # Drizzle table definitions (source of truth for migrations)
|
|
│ ├── client.ts # mysql2 pool + drizzle instance
|
|
│ └── migrations/ # Generated SQL migration files
|
|
├── routes/ # Hono route files (events, lists, push, sse, me, health)
|
|
├── auth/ # OIDC middleware
|
|
├── broker/ # CalDAV broker (tsdav + ical.js)
|
|
└── lib/ # Shared utilities
|
|
```
|
|
|
|
## Prerequisites
|
|
|
|
- **Node.js 22 LTS** — the Dockerfile base is `node:22-alpine`; match this locally
|
|
- **pnpm 11.5.1** — managed via corepack (`corepack enable pnpm`)
|
|
- **Docker + Docker Compose** — for MariaDB and Redis in dev
|
|
- **TypeScript 5.x** — installed per-workspace as a dev dependency
|
|
|
|
## Local Setup
|
|
|
|
### 1. Install dependencies
|
|
|
|
```bash
|
|
pnpm install
|
|
```
|
|
|
|
This installs all workspace packages (`apps/api` and `apps/pwa`) in a single pass.
|
|
|
|
### 2. Start the dev database and Redis
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb redis -d
|
|
```
|
|
|
|
The dev override (`docker-compose.dev.yml`) exposes MariaDB on `localhost:3306` and Redis on `localhost:6379`.
|
|
|
|
### 3. Configure environment variables
|
|
|
|
Copy the root `.env.example` to `.env` and fill in the required values. The root `.env` is sourced by docker-compose for container env vars. When running the API server directly on the host (outside Docker), you must override `DB_HOST`:
|
|
|
|
```bash
|
|
# Source the root .env, then override DB_HOST for host-side execution
|
|
DB_HOST=127.0.0.1 node dist/index.js
|
|
```
|
|
|
|
Or set `DB_HOST=127.0.0.1` in your local `.env` for the dev workflow. The docker-compose production config sets `DB_HOST: mariadb` (the service name); that value does not resolve on the host.
|
|
|
|
For auth bypass during local UI development, set `DEV_AUTH_BYPASS=true` — this skips OIDC and authenticates as the dev user (id 1).
|
|
|
|
## Development Workflow
|
|
|
|
### API dev loop
|
|
|
|
The `dev` script runs the compiled output via `node --watch`. **A build must exist before starting the dev server** — the watcher restarts `dist/index.js` on file changes, but it does not recompile TypeScript. You must rebuild when source changes.
|
|
|
|
```bash
|
|
# From apps/api — or use the root shortcut
|
|
pnpm --filter @familysync/api build # compile src/ → dist/
|
|
pnpm --filter @familysync/api dev # node --watch dist/index.js
|
|
|
|
# Root shortcuts
|
|
pnpm dev:api # runs dev in apps/api (requires dist/ to already exist)
|
|
```
|
|
|
|
Recommended inner loop: run `pnpm --filter @familysync/api build` after each change, the `--watch` process restarts automatically.
|
|
|
|
### PWA dev server
|
|
|
|
```bash
|
|
pnpm --filter @familysync/pwa dev
|
|
# or from root:
|
|
pnpm dev:pwa
|
|
```
|
|
|
|
Vite serves the PWA with HMR on the configured dev port. The PWA's API calls target the backend; set `VITE_API_URL` (or the Vite proxy config) to point at the running API.
|
|
|
|
## Build Commands
|
|
|
|
### Root workspace scripts
|
|
|
|
| Command | Description |
|
|
| ------------------- | ------------------------------------------------------------- |
|
|
| `pnpm dev:api` | Start API dev watcher (`node --watch dist/index.js`) |
|
|
| `pnpm dev:pwa` | Start Vite dev server for the PWA |
|
|
| `pnpm build` | Build both `apps/api` (tsc) and `apps/pwa` (tsc + vite build) |
|
|
| `pnpm test` | Run API test suite (`vitest run` in `apps/api`) |
|
|
| `pnpm test:e2e` | Run Playwright e2e harness (`apps/pwa`) |
|
|
| `pnpm lint` | ESLint across all workspaces (`pnpm -r --if-present lint`) |
|
|
| `pnpm format` | Reformat all files with Prettier (`prettier --write .`) |
|
|
| `pnpm format:check` | Check formatting without writing (`prettier --check .`) |
|
|
| `pnpm typecheck` | `tsc --noEmit` in all workspaces |
|
|
|
|
### `apps/api` scripts
|
|
|
|
| Command | Description |
|
|
| ------------------------------------------- | ----------------------------------------------- |
|
|
| `pnpm --filter @familysync/api build` | Compile TypeScript (`tsc`) → `dist/` |
|
|
| `pnpm --filter @familysync/api dev` | Start `node --watch dist/index.js` |
|
|
| `pnpm --filter @familysync/api start` | Start `node dist/index.js` (no watch) |
|
|
| `pnpm --filter @familysync/api test` | Run vitest once (`vitest run`) |
|
|
| `pnpm --filter @familysync/api test:watch` | Run vitest in watch mode |
|
|
| `pnpm --filter @familysync/api lint` | ESLint `src/` and `tests/` (`--max-warnings 0`) |
|
|
| `pnpm --filter @familysync/api typecheck` | `tsc --noEmit` |
|
|
| `pnpm --filter @familysync/api db:generate` | Generate SQL migrations from schema changes |
|
|
| `pnpm --filter @familysync/api db:migrate` | Apply pending migrations to the database |
|
|
|
|
### `apps/pwa` scripts
|
|
|
|
| Command | Description |
|
|
| ----------------------------------------------- | ----------------------------------------------------------- |
|
|
| `pnpm --filter @familysync/pwa dev` | Start Vite dev server with HMR |
|
|
| `pnpm --filter @familysync/pwa build` | `tsc && vite build` → `dist/` |
|
|
| `pnpm --filter @familysync/pwa preview` | Serve the production build locally |
|
|
| `pnpm --filter @familysync/pwa lint` | ESLint `src/` and `e2e/` (`--max-warnings 0`) |
|
|
| `pnpm --filter @familysync/pwa typecheck` | `tsc --noEmit` + `tsc --project tsconfig.e2e.json --noEmit` |
|
|
| `pnpm --filter @familysync/pwa test` | Run vitest once |
|
|
| `pnpm --filter @familysync/pwa test:e2e` | Run Playwright e2e tests |
|
|
| `pnpm --filter @familysync/pwa test:e2e:headed` | Playwright in headed mode (visible browser) |
|
|
| `pnpm --filter @familysync/pwa test:e2e:ui` | Playwright UI mode |
|
|
|
|
## Code Quality — Run Before Every Push
|
|
|
|
CI gates every PR to `main` on these checks. Run them locally before pushing to avoid a CI round-trip.
|
|
|
|
```bash
|
|
pnpm lint # ESLint --max-warnings 0 across apps/api (src/ + tests/) and apps/pwa (src/ + e2e/)
|
|
pnpm format:check # Prettier formatting check (use `pnpm format` to auto-fix)
|
|
pnpm typecheck # tsc --noEmit in both apps (includes apps/pwa tsconfig.e2e.json)
|
|
```
|
|
|
|
### ESLint
|
|
|
|
Config: `eslint.config.js` (root, flat ESLint 9 format). The config covers:
|
|
|
|
- **All `apps/**/\*.{ts,tsx}`** — `js.configs.recommended`+`tseslint.configs.recommendedTypeChecked`with`projectService: true`(type-aware rules, auto-discovers all`tsconfig.json` files)
|
|
- **`apps/pwa/**/\*.{ts,tsx}`additionally** —`eslint-plugin-react`+`eslint-plugin-react-hooks` (React 19 flat config; React Compiler rules disabled — this codebase does not use the Compiler)
|
|
- **Tool configs + test dirs** (`drizzle.config.ts`, `vitest.config.ts`, `apps/api/tests/**`, `apps/pwa/e2e/**`) — type-aware rules disabled via `disableTypeChecked` (these files are outside the main tsconfig projects)
|
|
- **Prettier integration** — `eslint-config-prettier` last in the config disables all formatting rules that conflict with Prettier
|
|
|
|
`--max-warnings 0` is enforced: warnings count as failures. **Blanket `eslint-disable` comments are not permitted** — every suppression requires a justification comment.
|
|
|
|
### Prettier
|
|
|
|
Config: `.prettierrc` (root). Settings: `singleQuote: true`, `semi: true`, `tabWidth: 2`, `trailingComma: "all"`, `printWidth: 100`.
|
|
|
|
```bash
|
|
pnpm format # write fixes in place
|
|
pnpm format:check # check only (used in CI)
|
|
```
|
|
|
|
The `.prettierignore` file at the repo root excludes build output and generated files.
|
|
|
|
## TypeScript Strict Checks
|
|
|
|
Both workspaces use `"strict": true` in their `tsconfig.json`. The build step (`tsc`) catches type errors in `apps/api` (since it emits output). For `apps/pwa`, Vite uses esbuild to transpile and does not perform type checking — **vitest will pass even when there are type errors in the PWA**. Always run the typecheck script explicitly:
|
|
|
|
```bash
|
|
# Check both workspaces
|
|
pnpm typecheck
|
|
|
|
# Or individually
|
|
pnpm --filter @familysync/api typecheck
|
|
pnpm --filter @familysync/pwa typecheck # also checks tsconfig.e2e.json
|
|
```
|
|
|
|
Run `pnpm typecheck` before opening a PR to catch errors that vitest and Vite builds will silently miss.
|
|
|
|
## CI Pipeline Overview
|
|
|
|
Every PR to `main` runs three parallel jobs (`.gitea/workflows/ci.yml`):
|
|
|
|
| Job | Checks |
|
|
| ------------- | ---------------------------------------------------------------------------------------------------- |
|
|
| `fast-checks` | `pnpm lint` → `pnpm format:check` → `pnpm typecheck` → `pnpm --filter @familysync/pwa test` |
|
|
| `api` | DB migrations + `pnpm --filter @familysync/api test` (vitest against a MariaDB 11 service container) |
|
|
| `harness` | DB migrations + API build + Playwright e2e (WebKit + Chromium) with `DEV_AUTH_BYPASS=true` |
|
|
|
|
All three jobs must pass before a PR can merge. See [docs/TESTING.md](TESTING.md) for test suite details.
|
|
|
|
## Drizzle Migration Workflow
|
|
|
|
Schema changes follow a strict two-step process. **`drizzle-kit push` is not available** — it has been removed from the scripts because it emits a false destructive diff (table truncation) on populated MariaDB databases.
|
|
|
|
### Step 1 — Generate the migration
|
|
|
|
After editing `apps/api/src/db/schema.ts`:
|
|
|
|
```bash
|
|
pnpm --filter @familysync/api db:generate
|
|
```
|
|
|
|
This runs `drizzle-kit generate` and writes a new `.sql` file to `apps/api/src/db/migrations/`. Review the generated SQL before proceeding — confirm it matches the intended schema change with no unexpected `DROP` or truncation statements.
|
|
|
|
### Step 2 — Apply the migration
|
|
|
|
```bash
|
|
pnpm --filter @familysync/api db:migrate
|
|
```
|
|
|
|
This runs `drizzle-kit migrate` and applies any pending migration files to the target database. Drizzle reads `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`, and optionally `DB_PORT` (default `3306`) from the environment, as defined in `apps/api/drizzle.config.ts`.
|
|
|
|
When running migrations from the host against the Docker database:
|
|
|
|
```bash
|
|
DB_HOST=127.0.0.1 pnpm --filter @familysync/api db:migrate
|
|
```
|
|
|
|
Migration files live in `apps/api/src/db/migrations/` and are committed to version control.
|
|
|
|
## Docker Compose Dev Stack
|
|
|
|
```bash
|
|
# Bring up the full dev stack (API in Docker + MariaDB + Redis, with ports exposed)
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
|
|
|
|
# Bring up only backing services (run API on host for faster iteration)
|
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb redis -d
|
|
```
|
|
|
|
The dev override:
|
|
|
|
- Exposes MariaDB on `localhost:3306`
|
|
- Exposes Redis on `localhost:6379`
|
|
- Mounts `apps/api/src` into the container for live source access
|
|
- Sets `NODE_ENV=development`
|
|
|
|
The production `docker-compose.yml` builds the API and PWA into a single image (`production` target in `apps/api/Dockerfile`). The PWA `dist/` is copied into the API image's `./public` directory and served on port `3000`.
|
|
|
|
## Common Issues
|
|
|
|
**`dev` script fails with "Cannot find module"**
|
|
The `dev` script runs `node --watch dist/index.js`. If `dist/` does not exist or is stale, run `pnpm --filter @familysync/api build` first.
|
|
|
|
**DB connection refused when running API on host**
|
|
`DB_HOST` defaults to `mariadb` (the Docker service name). When running the API outside Docker, override it: `DB_HOST=127.0.0.1`. The dev compose override exposes port 3306 on the host.
|
|
|
|
**drizzle-kit migrate says "cannot connect"**
|
|
Same `DB_HOST` issue. Prepend `DB_HOST=127.0.0.1` to the migrate command when running from the host.
|
|
|
|
**API integration tests fail with FK errors**
|
|
The vitest config sets `fileParallelism: false` to prevent concurrent test files from conflicting via the shared MariaDB. Ensure you are not overriding this. Tests require a running MariaDB — set `DB_HOST=127.0.0.1` and ensure the dev database is up.
|
|
|
|
**TypeScript errors missed during development**
|
|
Vite/esbuild strips types; type errors will not surface in `vitest run` or `vite build` output. Run `pnpm typecheck` explicitly to catch them.
|
|
|
|
**`pnpm lint` warns about ESLint version**
|
|
ESLint is pinned to 9.39.4. Do not upgrade to ESLint 10 until `eslint-plugin-react` resolves the `getFilename is not a function` incompatibility (`jsx-eslint#3977`).
|