mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-26 01:44:53 +10:00
239 lines
16 KiB
Markdown
239 lines
16 KiB
Markdown
# Coding Conventions
|
|
|
|
**Analysis Date:** 2026-05-11
|
|
|
|
This document is the canonical short-form reference for code style, structure, and feature-boundary rules in the Reactive Resume monorepo. The authoritative long-form reference lives in `AGENTS.md` at the repo root — when in doubt, defer to it.
|
|
|
|
## Tooling Stack
|
|
|
|
- **Formatter + linter:** Biome 2.x (`biome.json`). Pre-commit hook (`lefthook.yml`) runs `biome check --write --unsafe` on staged JS/TS/JSON files.
|
|
- **Type checker:** `tsgo --noEmit` (the `@typescript/native-preview` TS implementation) in every workspace package and `apps/web`. Use `pnpm typecheck` at the root or `pnpm --filter <pkg> typecheck` per package.
|
|
- **TypeScript base config:** `packages/config/tsconfig.base.json`, consumed by `tsconfig.json` at root and per-package `tsconfig.json` files.
|
|
- **Commit hook:** commitlint with `@commitlint/config-conventional` (`commitlint.config.cjs`). `body-max-line-length` is disabled.
|
|
- **Internal CLI:** `pnpm check` is **write-capable** (`biome check --write --unsafe .`). Use `biome check .` (no `--write`) for read-only inspection.
|
|
|
|
## Biome Configuration (`biome.json`)
|
|
|
|
**Formatter:**
|
|
- `lineWidth: 120`
|
|
- `indentStyle: "tab"`
|
|
- `javascript.formatter.quoteStyle: "double"`
|
|
- CSS parser has `tailwindDirectives: true`
|
|
|
|
**Linter rules (notable):**
|
|
- `recommended: true`
|
|
- `suspicious.noExplicitAny: "error"` — `any` is forbidden
|
|
- `suspicious.noArrayIndexKey: "off"`
|
|
- `correctness.useExhaustiveDependencies: "info"` (not an error)
|
|
- `style.useImportType: { level: "on", options: { style: "separatedType" } }` — `import type` is required when an import is type-only and must be on its own line (not inlined per-specifier)
|
|
- `style.noInferrableTypes: "error"` — drop redundant annotations like `const x: number = 1`
|
|
- `style.noUselessElse: "error"`
|
|
- `style.useSelfClosingElements: "error"`
|
|
- `style.useSingleVarDeclarator: "error"`
|
|
- `style.noParameterAssign: "error"`
|
|
- `style.useDefaultParameterLast: "error"`
|
|
- `nursery.useSortedClasses: { level: "warn", fix: "safe", functions: ["clsx", "cva", "cn"] }` — Tailwind class strings inside these wrappers are sorted automatically
|
|
|
|
**Import organization:** Biome's `assist.actions.source.organizeImports` is `on`, grouped in this order (`biome.json` lines 32-39):
|
|
|
|
1. Type-only imports (`{ "type": true }`)
|
|
2. Node built-ins (`":NODE:"`, excluding Bun)
|
|
3. Vitest + Testing Library (`vitest`, `vitest/**`, `@testing-library/**`)
|
|
4. External npm packages (`:PACKAGE:`, excluding `@reactive-resume/**`)
|
|
5. Internal workspace packages (`@reactive-resume/**`)
|
|
6. App-local aliases / relative paths (`:ALIAS:`, `:PATH:`)
|
|
|
|
A representative file that demonstrates the order: `packages/api/src/services/resume.ts` (type imports → node-free externals → `@reactive-resume/*` → relatives).
|
|
|
|
**Biome ignores:** `**/.turbo`, `**/.output`, `**/.vercel`, `**/.wrangler`, `**/coverage`, `**/reports`, `**/routeTree.gen.ts`.
|
|
|
|
## TypeScript Conventions
|
|
|
|
**Strictness flags** in `packages/config/tsconfig.base.json` are intentionally aggressive:
|
|
|
|
- `strict: true`
|
|
- `verbatimModuleSyntax: true` (pairs with Biome's `useImportType` enforcement)
|
|
- `exactOptionalPropertyTypes: true`
|
|
- `noUncheckedIndexedAccess: true`
|
|
- `noUncheckedSideEffectImports: true`
|
|
- `noUnusedLocals: true`, `noUnusedParameters: true`
|
|
- `noFallthroughCasesInSwitch: true`
|
|
- `isolatedModules: true`, `moduleResolution: "bundler"`
|
|
- `target: "ESNext"`, `module: "ESNext"`
|
|
|
|
**Type-only imports:** Always use `import type { … }` on its own line when an import is only used in type positions. See `packages/api/src/services/resume.ts:1-5` and `packages/api/src/context.ts:1-2`.
|
|
|
|
**`any`:** Banned by Biome (`noExplicitAny: "error"`). Use `unknown` and narrow, or use a discriminated union.
|
|
|
|
**Path aliases (web app only):** `apps/web/tsconfig.json` declares:
|
|
- `@/*` → `./src/*`
|
|
- `@reactive-resume/ui/*` → `../../packages/ui/src/*` (build-time alias for direct source resolution)
|
|
|
|
Internal packages do NOT use `@/*` aliases — they import siblings via relative paths and cross-package code via the `@reactive-resume/*` export maps.
|
|
|
|
## Package Export Conventions (source-consumed packages)
|
|
|
|
**Internal packages export `src` files directly via `package.json` `exports`.** There is no per-package build step; consumers pick up the TS source through bundler/vitest resolution. Do not assume any `dist/` output.
|
|
|
|
Sample (`packages/utils/package.json`):
|
|
|
|
```json
|
|
{
|
|
"name": "@reactive-resume/utils",
|
|
"type": "module",
|
|
"exports": {
|
|
"./color": "./src/color.ts",
|
|
"./date": "./src/date.ts",
|
|
"./resume/docx": "./src/resume/docx/index.ts",
|
|
"./resume/patch": "./src/resume/patch.ts",
|
|
"./url-security.node": "./src/url-security.node.ts"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Conventions when adding cross-package exports:**
|
|
|
|
- Use **explicit subpath exports**, not wildcards in `@reactive-resume/utils`/`@reactive-resume/db`. Some packages (`@reactive-resume/api`) do use wildcards like `"./services/*"` — match the style of the package you're editing.
|
|
- Filenames ending in `.node.ts` (e.g. `packages/utils/src/url-security.node.ts`, `packages/utils/src/monorepo.node.ts`) are reserved for Node-only code that must not be imported from the browser bundle.
|
|
- Filename suffixes `.browser.tsx` (e.g. `apps/web/src/components/resume/preview.browser.tsx`) mark code that must stay out of SSR paths.
|
|
|
|
## React & Web App Conventions
|
|
|
|
**Routing:** TanStack Router with file-based routes under `apps/web/src/routes`.
|
|
- Each route file calls `createFileRoute("/path")({ … })` and exports `Route`. Example: `apps/web/src/routes/auth/login.tsx:18`.
|
|
- `apps/web/src/routeTree.gen.ts` is generated — never hand-edit it (also Biome-ignored).
|
|
- Server-only handlers live in `server.handlers` blocks on routes like `apps/web/src/routes/api/rpc.$.ts`, `apps/web/src/routes/api/auth.$.ts`, `apps/web/src/routes/api/health.ts`.
|
|
- Public resume route `apps/web/src/routes/$username/$slug.tsx` is `ssr: "data-only"`; nested builder preview is `ssr: false`.
|
|
|
|
**Router context** (`apps/web/src/router.tsx`) provides `queryClient`, `orpc`, `theme`, `locale`, `session`, and `flags`. Read them via `Route.useRouteContext()` rather than refetching.
|
|
|
|
**Components:**
|
|
- Functional components only. React 19.
|
|
- File naming: **kebab-case** for files and directories (e.g. `apps/web/src/components/command-palette/`, `packages/ui/src/components/alert-dialog.tsx`, `apps/web/src/dialogs/api-key/create.tsx`).
|
|
- Test file: `<name>.test.ts(x)` colocated with the implementation (e.g. `packages/ui/src/components/button.tsx` + `packages/ui/src/components/button.test.tsx`).
|
|
- shadcn/Base UI primitive components in `packages/ui/src/components/*.tsx` are exported via the `./components/*` subpath. Hooks via `./hooks/*`.
|
|
|
|
**Tailwind class strings:** Always wrap in `clsx`, `cva`, or `cn` so Biome's `useSortedClasses` rule can sort them safely.
|
|
|
|
## oRPC Conventions (`packages/api`)
|
|
|
|
- **Procedures:** Build new procedures with `publicProcedure` or `protectedProcedure` from `packages/api/src/context.ts:79-99`. `protectedProcedure` adds the authenticated `User` to context and throws `ORPCError("UNAUTHORIZED")` otherwise; prefer it for anything authenticated.
|
|
- **Routers** live in `packages/api/src/routers/*.ts` and are composed in `packages/api/src/routers/index.ts` (`ai`, `auth`, `flags`, `resume`, `statistics`, `storage`). Each router file may export sub-routers internally (see `tagsRouter`, `statisticsRouter`, `analysisRouter`, `updatesRouter` in `packages/api/src/routers/resume.ts`).
|
|
- **Business logic** belongs in `packages/api/src/services/*.ts`. Handlers must stay thin: validate input, call a service, return its output. Example pattern: `packages/api/src/routers/resume.ts:24-26` calls `resumeService.tags.list(...)`.
|
|
- **DTOs / IO schemas** live in `packages/api/src/dto/*.ts` and are imported as `resumeDto.<op>.input` / `resumeDto.<op>.output`.
|
|
- **Errors:** Declare typed errors with `.errors({ CODE: { message, status } })` on the procedure (see `packages/api/src/routers/resume.ts:282-291`). Throw `new ORPCError("CODE")` inside services. The web side translates codes to user-facing strings in `apps/web/src/libs/error-message.ts`.
|
|
- **Route metadata:** Every procedure declares `.route({ method, path, tags, operationId, summary, description, successDescription })` so the OpenAPI/MCP endpoints stay accurate.
|
|
- **Rate limiting:** Apply via `.use(resumeMutationRateLimit)` / `.use(resumePasswordRateLimit)` from `packages/api/src/middleware/rate-limit`.
|
|
- **Web exposure:** Routers are mounted at `/api/rpc` by `apps/web/src/routes/api/rpc.$.ts`. The isomorphic client lives at `apps/web/src/libs/orpc/client.ts` — server-side calls use the in-process router client and browser calls hit `/api/rpc` with credentials.
|
|
|
|
## Drizzle Conventions (`packages/db`)
|
|
|
|
- **Schema location:** `packages/db/src/schema/*.ts`. Tables exported from `packages/db/src/schema/index.ts`.
|
|
- **Client:** `packages/db/src/client.ts`, exported as `@reactive-resume/db/client`.
|
|
- **Migrations:** Generated to **`migrations/` at the repo root** by `drizzle-kit generate`. Use `pnpm db:generate` after schema changes.
|
|
- **`DATABASE_URL` handling:** `drizzle-kit` does **not** auto-load `.env`. Always export `DATABASE_URL` in the shell (or prefix the command) before running `pnpm db:generate` / `pnpm db:migrate`.
|
|
- **Runtime migration:** `apps/web/plugins/1.migrate.ts` runs migrations on Nitro startup, so `pnpm db:migrate` is mostly used for first-time setup or debugging.
|
|
- **Patterns observed in `packages/db/src/schema/resume.ts`:**
|
|
- Primary keys use `pg.text("id").$defaultFn(() => generateId())` (UUIDv7 from `@reactive-resume/utils/string`).
|
|
- `createdAt` / `updatedAt` use `withTimezone: true`, `.defaultNow()`, and `$onUpdate(() => new Date())`.
|
|
- JSONB columns get `.$type<T>()` for end-to-end typing.
|
|
- Composite uniques/indexes are declared in the table's tuple callback.
|
|
- Foreign keys use `onDelete: "cascade"`.
|
|
|
|
## Schema-First Change Workflow
|
|
|
|
When changing resume data shape, propagate in this order (per `AGENTS.md`):
|
|
|
|
1. **`packages/schema/src/resume/*.ts`** — Zod schemas and types (entry point).
|
|
2. **`packages/api/src/dto/*.ts`** — API DTOs that re-use those schemas.
|
|
3. **`packages/import/src/*.tsx`** — importers (`json-resume`, `reactive-resume-json`, `reactive-resume-v4-json`).
|
|
4. **`packages/pdf/src/templates/**`** — PDF rendering for every template (`azurill`, `bronzor`, `chikorita`, `ditgar`, `ditto`, `gengar`, `glalie`, `kakuna`, `lapras`, `leafish`, `meowth`, `onyx`, `pikachu`, `rhyhorn`, `scizor`). Shared filtering: `packages/pdf/src/templates/shared/filtering.ts`.
|
|
5. **`apps/web/src/`** — builder forms and any consumer hooks.
|
|
|
|
Adding/renaming a template requires changes in `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, the template directory `packages/pdf/src/templates/<name>/`, and static previews under `apps/web/public/templates/{jpg,pdf}/`.
|
|
|
|
## Error Handling Patterns
|
|
|
|
- **Services:** Throw `new ORPCError("CODE")` (e.g. `NOT_FOUND`, `UNAUTHORIZED`, custom `RESUME_LOCKED`). Example: `packages/api/src/services/resume.ts:54`.
|
|
- **Routers:** Declare expected codes via `.errors({ … })` so callers get typed error narrowing.
|
|
- **Auth helpers** in `packages/api/src/context.ts:14-55` catch verification errors and `console.warn(...)` rather than throwing, returning `null` so the caller can fall through to the next auth method.
|
|
- **Web side:** `apps/web/src/libs/error-message.ts` exposes `getReadableErrorMessage`, `getOrpcErrorMessage`, and `getResumeErrorMessage` for translating raw errors into UI-safe strings. Pair with `sonner` toasts (see `apps/web/src/routes/auth/login.tsx:45-64`).
|
|
|
|
## Logging
|
|
|
|
- No dedicated logging framework. Use `console.warn` / `console.error` for diagnostic output, scoped tightly (see `packages/api/src/context.ts:25`).
|
|
- Server logs are also where dev-mode email verification links surface when SMTP is unconfigured.
|
|
|
|
## i18n & Translations (Lingui)
|
|
|
|
- **Library:** `@lingui/core`, `@lingui/react` with the babel macro plugin enabled in `apps/web/vitest.config.ts` and Vite config.
|
|
- **Config:** `apps/web/lingui.config.ts` — source locale `en-US`, pseudo locale `zu-ZA`, 50+ supported locales. Catalogs live in `apps/web/locales/{locale}.po`.
|
|
- **Usage:**
|
|
- Import macros: `import { t } from "@lingui/core/macro"` and `import { Trans } from "@lingui/react/macro"` (`apps/web/src/routes/auth/login.tsx:1-2`).
|
|
- Wrap displayed text in `<Trans>...</Trans>` for JSX or `` t`...` `` / `t({ message, comment })` for strings.
|
|
- Provide `comment:` for ambiguous fallback strings (see `login.tsx:57-60`).
|
|
- **Extraction:** `pnpm lingui:extract` (turbo task in `apps/web`). Crowdin sync runs via `.github/workflows/crowdin-sync.yml`.
|
|
|
|
## Comments Policy
|
|
|
|
Comments stay short and explain **why**, not what. Patterns observed:
|
|
|
|
- One-line `//` comments before a non-obvious decision (`vitest.setup.ts:5-7` explains why `cleanup()` is registered manually; `packages/utils/src/monorepo.node.test.ts:11` explains the `realpathSync` call).
|
|
- JSDoc/TSDoc only on cross-package public functions where intent matters (e.g. `packages/api/src/context.ts:57-63` documents `resolveUserFromRequestHeaders`).
|
|
- Inline `/* @__PURE__ */` annotations on `$onUpdate(() => new Date())` in Drizzle schemas (`packages/db/src/schema/resume.ts:36`).
|
|
- No commented-out code in commits.
|
|
|
|
## Git Hooks & Commit Style
|
|
|
|
**Lefthook (`lefthook.yml`):**
|
|
|
|
```yaml
|
|
pre-commit:
|
|
parallel: true
|
|
jobs:
|
|
- name: lint and format
|
|
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
|
|
run: pnpm biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
|
|
stage_fixed: true
|
|
|
|
commit-msg:
|
|
jobs:
|
|
- name: commitlint
|
|
run: pnpm commitlint --edit {1}
|
|
```
|
|
|
|
The pre-commit hook **rewrites staged files** with Biome fixes (`stage_fixed: true`). Run `pnpm check` before staging to avoid surprises.
|
|
|
|
**Commit messages:** Conventional Commits (`commitlint.config.cjs` extends `@commitlint/config-conventional`). Examples from `git log`:
|
|
|
|
- `feat: implement an AI chat window for agentic resume building`
|
|
- `fix(pdf): register CJK fallback font so Chinese/Japanese/Korean text renders correctly`
|
|
- `fix(lapras): adjust lapras border color to fixed gray`
|
|
- `chore: migrate from jsdom to happy-dom for testing environment`
|
|
- `docs: update AGENTS.md with detailed codebase structure`
|
|
- `test: add unit and component tests across the monorepo`
|
|
- `chore(release): v5.1.2`
|
|
|
|
Allowed types: `feat`, `fix`, `chore`, `docs`, `test`, `refactor`, `style`, `perf`, `build`, `ci`, `revert`. Scope is optional and lowercase. `body-max-line-length` is disabled, so long PR bodies are fine.
|
|
|
|
## CI Workflows
|
|
|
|
- **`.github/workflows/autofix.yml`** runs on every PR and push to `main`: `pnpm install --frozen-lockfile` → `pnpm knip --fix` (prune unused deps) → `pnpm check` (Biome) → `autofix-ci/action` opens fix commits.
|
|
- **`.github/workflows/docker-build.yml`** is `workflow_dispatch` only, builds multi-arch Docker images.
|
|
- **`.github/workflows/crowdin-sync.yml`** syncs translation catalogs.
|
|
- There is no CI workflow that runs `pnpm test` today. Tests run locally via `pnpm test` / `pnpm test:ci` and through turbo's `test:agent` reporter for agent-driven runs.
|
|
|
|
## Function & Module Design
|
|
|
|
- **Function size:** Most service functions stay under ~40 lines. Bigger flows (e.g. `resumeService.patch`) are decomposed into helpers in `packages/api/src/helpers/*` and `packages/api/src/services/resume-events.ts`.
|
|
- **Parameters:** Service helpers consistently take a single object argument (`async ({ id, userId })`) rather than positional args. See `packages/api/src/services/resume.ts:27,41,65`.
|
|
- **Return values:** Services return plain typed objects; routers shape the response via `.output(schema)` so Zod validates at the boundary.
|
|
- **Module boundaries:**
|
|
- Cross-package imports must go through declared `exports` subpaths. Reaching into `packages/<x>/src/internal-file` directly is not allowed.
|
|
- `packages/utils` exports are narrowly scoped — if you need a new helper for another package, add a new explicit subpath in `packages/utils/package.json`.
|
|
- `packages/runtime-externals` and `packages/scripts` are support packages; avoid importing them into runtime code.
|
|
|
|
---
|
|
|
|
*Convention analysis: 2026-05-11*
|