This document covers the tools and practices for maintaining code quality.
| Tool | Purpose | Config |
|---|---|---|
| ESLint | Linting | eslint.config.js |
| Prettier | Code formatting | .prettierrc |
| TypeScript | Type checking | tsconfig.json |
| Lefthook | Git hooks | lefthook.yml |
# Linting
bun run lint # Lint and auto-fix
# Formatting
bun run format # Format all files
bun run format:check # Check formatting (CI)
# Type Checking
bun run typecheck # Run TypeScript compiler
# All checks
bun run lint && bun run format:check && bun run typecheckESLint is configured in eslint.config.js with:
- TypeScript support
- React/React Hooks rules
- Import sorting
- Accessibility (jsx-a11y)
# Lint with auto-fix
bun run lint
# Lint specific files
bunx eslint "app/routes/**/*.tsx" --fix| Rule | Description |
|---|---|
@typescript-eslint/no-explicit-any |
Disallow any type |
react-hooks/rules-of-hooks |
Enforce hooks rules |
react-hooks/exhaustive-deps |
Verify dependency arrays |
import/order |
Consistent import ordering |
// Single line
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = response;
// Block
/* eslint-disable @typescript-eslint/no-explicit-any */
// ... code
/* eslint-enable @typescript-eslint/no-explicit-any */
// File-level (at top)
/* eslint-disable @typescript-eslint/no-explicit-any */Note: Prefer fixing the issue over disabling rules.
Prettier config in .prettierrc:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSameLine": true
}# Format all files
bun run format
# Format specific files
bunx prettier --write "app/routes/**/*.tsx"
# Check without writing
bun run format:checkInstall Prettier extension and enable "Format on Save":
VS Code settings:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}TypeScript is configured in tsconfig.json with strict mode enabled.
bun run typecheckThis runs:
react-router typegen- Generate route typestsc- TypeScript compiler
| Option | Effect |
|---|---|
strict: true |
Enable all strict checks |
noImplicitAny |
Error on implicit any |
strictNullChecks |
Nullable types must be handled |
noUnusedLocals |
Error on unused variables |
// Bad: Using any
const data: any = response;
// Good: Define proper type
interface ApiResponse {
items: Organization[];
}
const data: ApiResponse = response;
// Good: Use unknown + type guard
const data: unknown = response;
if (isApiResponse(data)) {
// data is typed here
}// Avoid: Type assertion
const org = data as Organization;
// Prefer: Type guard
function isOrganization(obj: unknown): obj is Organization {
return typeof obj === 'object' && obj !== null && 'id' in obj;
}
if (isOrganization(data)) {
// data is Organization
}Git hooks are configured in lefthook.yml:
pre-commit:
parallel: true
commands:
lint:
glob: '*.{ts,tsx}'
run: bunx eslint {staged_files} --fix
format:
glob: '*.{ts,tsx,json,css,md}'
run: bunx prettier --write {staged_files}
types:
run: bun run typecheckOn git commit:
- ESLint runs on staged
.ts/.tsxfiles - Prettier formats staged files
- TypeScript check runs
- If any fail, commit is blocked
# Skip hooks (not recommended)
git commit --no-verify -m "message"Note: Only bypass for emergencies. Fix the issue properly.
Imports should be ordered:
- External packages
- Internal aliases (
@/,@shadcn/,@datum-ui/) - Relative imports
- Type imports
// 1. External
// 3. Relative
import { PageHeader } from './components/page-header';
import { useOrganizations } from '@/resources/organizations';
// 4. Types
import type { Organization } from '@/resources/organizations';
import { DataTable } from '@datum-ui/components/data-table';
// 2. Internal aliases
import { Button } from '@shadcn/ui/button';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';Use aliases instead of relative paths:
// Good
import { Button } from '@shadcn/ui/button';
import { env } from '@/utils/env';
// Avoid
import { Button } from '../../../modules/shadcn/ui/button';
import { env } from '../../utils/env';| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | PageHeader.tsx |
| Hooks | camelCase with use |
useOrganizations.ts |
| Utilities | camelCase | formatDate.ts |
| Types | PascalCase | Organization.ts |
| Constants | SCREAMING_SNAKE | API_ENDPOINTS.ts |
// 1. Imports
import { useState } from 'react';
// 2. Types
interface Props {
title: string;
onSubmit: () => void;
}
// 3. Component
export function MyComponent({ title, onSubmit }: Props) {
// Hooks first
const [state, setState] = useState('');
// Handlers
const handleClick = () => {
// ...
};
// Render
return (
<div>
<h1>{title}</h1>
<button onClick={handleClick}>Submit</button>
</div>
);
}anytypes - useunknownwith type guards- Magic numbers - use named constants
- Deeply nested code - extract functions
- Large files - split into modules
- Console.log in production - use logger
Quality checks run in GitHub Actions:
# .github/workflows/quality-checks.yml
jobs:
quality:
steps:
- name: Lint
run: bun run lint
- name: Format Check
run: bun run format:check
- name: Type Check
run: bun run typecheckPRs are blocked if any check fails.
# Verify config exists
ls eslint.config.js
# Run with debug
DEBUG=eslint:* bunx eslint app/Ensure Prettier runs after ESLint:
# In lefthook or CI
bun run lint && bun run format# Regenerate route types
bun run typecheck
# If still failing, restart TS server
# VS Code: Cmd+Shift+P → "TypeScript: Restart TS Server"- Testing - Test quality
- Project Structure - Code organization