feature/pre-commit-checks #5

Merged
ksv741 merged 5 commits from feature/pre-commit-checks into main 2026-06-14 09:11:53 +03:00
13 changed files with 2407 additions and 18 deletions

1
.husky/pre-commit Executable file
View File

@ -0,0 +1 @@
npx lint-staged

View File

@ -0,0 +1,27 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
plugins: ['@typescript-eslint/eslint-plugin', 'react', 'react-hooks'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
],
root: true,
env: {
browser: true,
es2020: true,
},
settings: {
react: { version: 'detect' },
},
ignorePatterns: ['.eslintrc.cjs', 'vite.config.ts', 'vitest.config.ts', 'dist/'],
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'react/react-in-jsx-scope': 'off',
},
};

View File

@ -8,6 +8,7 @@
"build": "tsc -b && vite build",
"preview": "vite preview",
"codegen": "openapi-typescript http://localhost:3000/api/docs-json -o src/api/types.ts",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"test": "vitest run",
"test:watch": "vitest"
},
@ -20,6 +21,11 @@
"react-router-dom": "^6.20.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.0.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",

View File

@ -1,13 +1,9 @@
import { describe, it, expect } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { http, HttpResponse } from 'msw';
import { server } from '../test/server';
import { useBond } from './useBond';
import { type ReactNode } from 'react';
const API = '/api/v1';
function createWrapper() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return function Wrapper({ children }: { children: ReactNode }) {

View File

@ -1,13 +1,9 @@
import { describe, it, expect } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { http, HttpResponse } from 'msw';
import { server } from '../test/server';
import { useStock } from './useStock';
import { type ReactNode } from 'react';
const API = '/api/v1';
function createWrapper() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return function Wrapper({ children }: { children: ReactNode }) {

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { Routes, Route } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
@ -9,10 +9,6 @@ import { renderWithProviders } from '../test/test-utils';
const API = '/api/v1';
async function waitForAuth() {
await screen.findByText('Войти');
}
describe('LoginPage', () => {
it('renders login form', async () => {
server.use(http.post(`${API}/auth/refresh`, () => new HttpResponse(null, { status: 401 })));

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../test/server';

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { Routes, Route } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';

View File

@ -29,7 +29,10 @@ export function renderWithProviders(
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[route]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter
initialEntries={[route]}
future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
>
<AuthProvider>{children}</AuthProvider>
</MemoryRouter>
</QueryClientProvider>

View File

@ -0,0 +1,187 @@
# Pre-commit Checks Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Добавить pre-commit хуки (ESLint + Prettier) для backend и frontend, блокирующие коммит при ошибках.
**Architecture:** Husky + lint-staged в корне монорепозитория. ESLint 8 (совместим с существующим backend) для обеих workspace. Frontend получает свой `.eslintrc.cjs` с React-правилами.
**Tech Stack:** Husky 9, lint-staged 15, ESLint 8, Prettier 3
---
### Task 1: ESLint для фронтенда
**Файлы:**
- Создать: `apps/frontend/.eslintrc.cjs`
- Изменить: `apps/frontend/package.json` (scripts + devDependencies)
- [ ] **Step 1: Добавить devDependencies в `apps/frontend/package.json`**
В секцию `devDependencies` добавить:
```json
"eslint": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0"
```
- [ ] **Step 2: Добавить скрипт lint в `apps/frontend/package.json`**
```json
"lint": "eslint \"src/**/*.{ts,tsx}\""
```
- [ ] **Step 3: Создать `apps/frontend/.eslintrc.cjs`**
```js
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
plugins: ['@typescript-eslint/eslint-plugin', 'react', 'react-hooks'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
],
root: true,
env: {
browser: true,
es2020: true,
},
settings: {
react: { version: 'detect' },
},
ignorePatterns: ['.eslintrc.cjs', 'vite.config.ts', 'vitest.config.ts', 'dist/'],
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'react/react-in-jsx-scope': 'off',
},
};
```
- [ ] **Step 4: Проверить, что ESLint работает на фронтенде**
Run:
```bash
npm run lint -w apps/frontend
```
Expected: ESLint проверяет все `.ts,.tsx` файлы в `apps/frontend/src/`. Если есть ошибки — мы их фиксим. Если ошибок нет — чистый выход.
- [ ] **Step 5: Commit**
```bash
git add apps/frontend/package.json apps/frontend/.eslintrc.cjs
git commit -m "feat: add ESLint config for frontend"
```
---
### Task 2: Husky + lint-staged
**Файлы:**
- Изменить: `package.json` (корень) — devDependencies + lint-staged config
- Создать: `.husky/pre-commit`
- Создать: `.husky/_/` (содержимое от `husky init`)
- [ ] **Step 1: Установить husky и lint-staged в корень**
Run:
```bash
npm install --save-dev husky lint-staged
```
- [ ] **Step 2: Инициализировать Husky**
Run:
```bash
npx husky init
```
Это создаст `.husky/` директорию с `pre-commit` хуком.
- [ ] **Step 3: Добавить lint-staged config в корневой `package.json`**
В корневой `package.json` добавить (после `devDependencies`):
```json
"lint-staged": {
"apps/backend/src/**/*.ts": ["eslint --max-warnings=0"],
"apps/backend/test/**/*.ts": ["eslint --max-warnings=0"],
"apps/frontend/src/**/*.{ts,tsx}": ["eslint --max-warnings=0"],
"*.{ts,tsx}": ["prettier --check"]
}
```
- [ ] **Step 4: Настроить `.husky/pre-commit`**
Проверить содержимое `.husky/pre-commit`:
```bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
```
Если `husky init` уже создал подходящий файл, оставить как есть. Убедиться, что вызов `npx lint-staged` присутствует.
- [ ] **Step 5: Commit**
```bash
git add package.json .husky/
git commit -m "feat: add Husky pre-commit hook with lint-staged"
```
---
### Task 3: Обновить root lint script
**Файлы:**
- Изменить: `package.json` (корень) — секция scripts
- [ ] **Step 1: Обновить `lint` скрипт в корневом `package.json`**
Найти строку:
```json
"lint": "npm run lint -w apps/backend",
```
Заменить на:
```json
"lint": "npm run lint -w apps/backend && npm run lint -w apps/frontend",
```
- [ ] **Step 2: Проверить, что корневой lint работает**
Run:
```bash
npm run lint
```
Expected: ESLint проходит по backend и frontend, возвращает 0 при отсутствии ошибок.
- [ ] **Step 3: Проверить chain целиком (опционально)**
Протестировать pre-commit hook:
```bash
git add . && git commit -m "test pre-commit hook"
```
Должен выполнить lint-staged, проверить ESLint + Prettier на staged файлах.
- [ ] **Step 4: Commit**
```bash
git add package.json
git commit -m "chore: update root lint script to cover both workspaces"
```

View File

@ -0,0 +1,48 @@
# Pre-commit Checks: ESLint + Prettier для монорепозитория
## Дата
2026-06-14
## Проблема
При коммите нет локальных проверок кода. Ошибки линтера и форматирования обнаруживаются только вручную или на CI (которого пока нет).
## Решение
Husky + lint-staged для pre-commit хука, который проверяет ESLint и Prettier только на staged файлах и блокирует коммит при ошибках.
## Схема работы
git commit → husky pre-commit → lint-staged
├── staged *.ts (apps/backend) → eslint --max-warnings=0
├── staged *.ts/tsx (apps/frontend) → eslint --max-warnings=0
└── staged *.ts/tsx (все) → prettier --check (с выводом diff)
→ любая ошибка → commit BLOCKED
## Состав изменений
### 1. Frontend ESLint
- Установить в `apps/frontend` (локально): `eslint`, `@eslint/js`, `typescript-eslint`, `eslint-plugin-react-hooks`, `eslint-plugin-react`
- Создать `apps/frontend/eslint.config.js` (flat config)
- Добавить `"lint": "eslint src/"` в `apps/frontend/package.json`
### 2. Husky + lint-staged
- Установить в корень: `husky`, `lint-staged`
- `npx husky init` → создать `.husky/pre-commit`
- Конфиг lint-staged в корневом `package.json`:
- `apps/backend/src/**/*.ts``eslint --max-warnings=0`
- `apps/frontend/src/**/*.ts{,x}``eslint --max-warnings=0`
- `*.{ts,tsx}``prettier --check`
### 3. Root lint script
- Расширить на оба workspace: `"lint": "npm run lint -w apps/backend && npm run lint -w apps/frontend"`
## Поведение при ошибках
- ESLint: показывает ошибки, commit blocked
- Prettier: показывает diff, commit blocked. Разработчик запускает `npm run format` вручную.
## Файлы для создания
- `apps/frontend/eslint.config.js`
- `.husky/pre-commit`
## Файлы для изменения
- `apps/frontend/package.json` — scripts + devDependencies
- Корневой `package.json` — lint-staged config + lint script + devDependencies

2120
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,13 +13,22 @@
"build:frontend": "npm run build -w apps/frontend",
"test:backend": "npm run test -w apps/backend",
"test:frontend": "npm run test -w apps/frontend",
"lint": "npm run lint -w apps/backend",
"lint": "npm run lint -w apps/backend && npm run lint -w apps/frontend",
"format": "prettier --write \"**/*.{ts,tsx}\"",
"format:check": "prettier --check \"**/*.{ts,tsx}\"",
"dev:docs": "npm run dev -w apps/docs",
"build:docs": "npm run build -w apps/docs"
"build:docs": "npm run build -w apps/docs",
"prepare": "husky"
},
"lint-staged": {
"apps/backend/src/**/*.ts": ["eslint --max-warnings=0"],
"apps/backend/test/**/*.ts": ["eslint --max-warnings=0"],
"apps/frontend/src/**/*.{ts,tsx}": ["eslint --max-warnings=0"],
"**/*.{ts,tsx}": ["prettier --check"]
},
"devDependencies": {
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"prettier": "^3.0.0"
}
}