Biome is the primary tool for both formatting and linting. There is no Prettier.
Configuration lives in biome.json at the repo root.
| Setting | Value |
|---|---|
| Line width | 120 |
| Indent style | Spaces |
| Indent width | 2 |
Run the formatter:
pnpm format # biome check --write .
Biome's recommended rules are enabled. Notable additions:
| Rule | Level | Purpose |
|---|---|---|
noUndeclaredEnvVars (nursery) |
warn | Encourages documenting env variable usage |
noRestrictedImports (apps/web) |
error | Prevents importing @repo/db directly in the web app |
Test files (*.test.ts, *.test.tsx) have relaxed rules:
noExplicitAny: offnoNonNullAssertion: offThis allows pragmatic test code without fighting the linter.
Biome automatically organizes imports into groups separated by blank lines:
// 1. Node builtins, third-party packages (excluding @repo)
import { z } from "zod";
import { TRPCError } from "@trpc/server";
// 2. Internal @repo/* packages
import { patientsRepo } from "@repo/db";
import { authenticatedProcedure } from "@repo/trpc/procedures";
// 3. App-level aliases (~/)
import { Providers } from "~/lib/providers";
// 4. Relative imports
import { PatientForm } from "./components/PatientForm";
The groups are configured in biome.json:
{
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
[
":NODE:",
":PACKAGE:",
":PACKAGE_WITH_PROTOCOL:",
":URL:",
"!@repo/**"
],
":BLANK_LINE:",
"@repo/**",
":BLANK_LINE:",
"~/**",
":PATH:"
]
}
}
}
}
}
}
Test files have their own import order: mocks first, then vitest/test utilities, then everything else.
ESLint is used only for Next.js-specific rules in apps/web. The config is in apps/web/eslint.config.mjs and uses @next/eslint-plugin-next (recommended + core-web-vitals).
The lint script in apps/web runs both Biome and ESLint:
{
"lint": "biome check . && next lint"
}
All packages use TypeScript strict mode. The shared configs live in packages/typescript-config/:
| Config | Used By | Key Settings |
|---|---|---|
base.json |
Most packages | strict: true, target: ES2022, module: NodeNext, noUncheckedIndexedAccess: true |
nextjs.json |
apps/web |
Extends base; module: ESNext, moduleResolution: Bundler, jsx: preserve |
react-library.json |
React packages | Extends base with React/JSX support |
strict: true: All strict checks enabled.noUncheckedIndexedAccess: true: Array/object index access returns T | undefined.isolatedModules: true: Ensures compatibility with transpilers.declaration: true + declarationMap: true: Generates .d.ts files for package consumers.pnpm check-types # Runs tsc --noEmit across all packages via Turbo
Task definitions live in turbo.json:
| Task | Dependencies | Cached | Notes |
|---|---|---|---|
build |
^build |
Yes | Outputs: .next/**; env vars included in cache key |
dev |
-- | No | Persistent task |
test |
^build |
Yes | Inputs: **/*.test.{ts,tsx}, .env* |
test:watch |
-- | No | Persistent task |
lint |
^lint |
Yes | |
check-types |
^check-types |
Yes | |
storybook:build |
^build |
Yes | Outputs: storybook-static/** |
Build and test cache keys include environment variables to ensure rebuilds when config changes:
BASE_URL, SUPABASE_URL, SUPABASE_ANON_KEY, DATABASE_URLNEXT_PUBLIC_TRPC_URL, NEXT_PUBLIC_OAUTH_REDIRECT_URLturbo.json env arrays).Defined in pnpm-workspace.yaml:
packages:
- "apps/*"
- "packages/*"
The root package.json pins zod to 4.3.6 across the workspace:
{
"pnpm": {
"overrides": {
"zod": "4.3.6"
}
}
}
{
"engines": {
"node": ">=20"
},
"packageManager": "pnpm@10.23.0"
}
Next: Testing