feat: initial dev-configs monorepo

Shared configs for TypeScript projects: ESLint, Prettier, TypeScript,
Vite, Jest, Playwright, Knip. Published as @vigdorov/* npm packages
to Gitea registry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 23:40:22 +03:00
commit cf64bf6d7d
38 changed files with 11287 additions and 0 deletions

51
.drone.yml Normal file
View File

@ -0,0 +1,51 @@
## Library CI pipeline — for npm package publishing
## Configure project via service.yaml
kind: pipeline
type: kubernetes
name: ci
steps:
- name: prepare
image: alpine:3.19
environment:
GITEA_TOKEN:
from_secret: GITEA_TOKEN
commands:
- apk add --no-cache git bash yq
- git clone --depth 1 https://token:$GITEA_TOKEN@git.vigdorov.ru/vigdorov/ci-templates.git .ci
- chmod +x .ci/scripts/*.sh
- bash .ci/scripts/prepare.sh
- name: install
image: node:22-alpine
depends_on: [prepare]
commands:
- npm ci
- name: test
image: node:22-alpine
depends_on: [install]
commands:
- npm run lint
- npm run test
- npm run check
- name: build
image: node:22-alpine
depends_on: [test]
commands:
- npm run build
- name: publish
image: node:22-alpine
depends_on: [build]
environment:
GITEA_TOKEN:
from_secret: GITEA_TOKEN
commands:
- bash .ci/scripts/publish-lib.sh
trigger:
branch: [main]
event: [push]

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
dist/
coverage/
.ci/
*.tsbuildinfo

1
.npmrc Normal file
View File

@ -0,0 +1 @@
@vigdorov:registry=https://git.vigdorov.ru/api/packages/vigdorov/npm/

58
CLAUDE.md Normal file
View File

@ -0,0 +1,58 @@
# dev-configs
Monorepo с общими конфигами для TypeScript-проектов. Публикуется как набор npm-пакетов в Gitea npm registry.
## Технологии
- **Monorepo:** npm workspaces
- **Сборка:** TypeScript (tsc)
- **Registry:** Gitea npm (`@vigdorov/` scope)
- **CI:** Drone CI, тип `library`
## Пакеты
| Пакет | Тип | Описание |
|-------|-----|----------|
| `@vigdorov/prettier-config` | Статичный объект | Prettier конфиг |
| `@vigdorov/eslint-config` | Функции-генераторы (base, react, node) | ESLint 9 flat config |
| `@vigdorov/typescript-config` | JSON для extends (base.json, react.json) | TSConfig |
| `@vigdorov/vite-config` | Функции-генераторы (spa, library) | Vite конфиг |
| `@vigdorov/jest-config` | Функция-генератор (node/jsdom) | Jest + @swc/jest |
| `@vigdorov/playwright-config` | Функция-генератор (CI-aware) | Playwright e2e |
| `@vigdorov/knip-config` | Функция-генератор | Dead code detection |
## Структура
```
packages/
├── eslint/ → @vigdorov/eslint-config
├── prettier/ → @vigdorov/prettier-config
├── typescript/ → @vigdorov/typescript-config (без сборки, JSON-файлы)
├── vite/ → @vigdorov/vite-config
├── jest/ → @vigdorov/jest-config
├── playwright/ → @vigdorov/playwright-config
└── knip/ → @vigdorov/knip-config
```
## Команды
```bash
npm run build # Собрать все пакеты
npm run test # Тесты всех пакетов
npm run lint # Линтинг всех пакетов
npm run check # Knip проверка всех пакетов
# Поднятие версии
npm version patch -w packages/eslint
npm version minor -w packages/vite
```
## Версионирование
- Semver, независимое для каждого пакета
- Разработка в feature-ветке → merge в main → поднять версию → push
- CI публикует только пакеты с новой версией (сравнение с registry)
## Требования
Полные требования: [docs/requirements.md](docs/requirements.md)

211
README.md Normal file
View File

@ -0,0 +1,211 @@
# @vigdorov/dev-configs
Набор общих конфигов для TypeScript-проектов. Единые правила линтинга, форматирования, сборки и тестирования.
## Установка
Добавь в `.npmrc` проекта:
```ini
@vigdorov:registry=https://git.vigdorov.ru/api/packages/vigdorov/npm/
```
Затем устанавливай нужные пакеты:
```bash
npm i -D @vigdorov/prettier-config @vigdorov/eslint-config @vigdorov/typescript-config
```
---
## Пакеты
### @vigdorov/prettier-config
Единый стиль форматирования: 120 символов, 4 пробела, одинарные кавычки.
```js
// prettier.config.js
import config from '@vigdorov/prettier-config';
export default config;
```
### @vigdorov/eslint-config
ESLint 9 flat config. Три пресета: `base` (любой TS-проект), `react` (фронтенд), `node` (бэкенд).
```js
// eslint.config.js — React-проект
import {react} from '@vigdorov/eslint-config';
export default react();
```
```js
// eslint.config.js — NestJS-проект
import {node} from '@vigdorov/eslint-config';
export default node();
```
Кастомизация через callback:
```js
import {react} from '@vigdorov/eslint-config';
export default react((configs) => [
...configs,
{
rules: {
'no-console': 'off',
},
},
]);
```
### @vigdorov/typescript-config
Базовые tsconfig для наследования.
```jsonc
// tsconfig.json — React-проект
{
"extends": "@vigdorov/typescript-config/react",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
```
```jsonc
// tsconfig.json — Node/NestJS-проект
{
"extends": "@vigdorov/typescript-config/base",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
```
### @vigdorov/vite-config
Конфиг Vite для SPA-приложений и библиотек.
```ts
// vite.config.ts — SPA
import {spa} from '@vigdorov/vite-config';
export default spa({
port: 5176,
aliases: {'@': 'src'},
proxy: {'/api': 'http://localhost:3003'},
});
```
```ts
// vite.config.ts — библиотека
import {library} from '@vigdorov/vite-config';
export default library({
entry: 'src/index.ts',
name: 'my-lib',
external: ['react', 'react-dom'],
});
```
| Параметр (spa) | По умолчанию | Описание |
|----------------|-------------|----------|
| `port` | `5173` | Порт dev-сервера |
| `aliases` | — | Алиасы путей (`{'@': 'src'}`) |
| `proxy` | — | Проксирование запросов |
| `base` | `'/'` | Base URL |
| `outDir` | `'dist'` | Папка сборки |
| Параметр (library) | По умолчанию | Описание |
|--------------------|-------------|----------|
| `entry` | — | Точка входа (обязательный) |
| `name` | — | Имя библиотеки (обязательный) |
| `aliases` | — | Алиасы путей |
| `external` | `[]` | Зависимости, исключаемые из бандла |
| `formats` | `['es', 'cjs']` | Форматы выходных файлов |
### @vigdorov/jest-config
Jest с быстрой трансформацией через SWC. Алиасы в едином формате с Vite.
```ts
// jest.config.ts
import {jestConfig} from '@vigdorov/jest-config';
export default jestConfig({
environment: 'jsdom',
aliases: {'@': 'src'},
});
```
| Параметр | По умолчанию | Описание |
|----------|-------------|----------|
| `environment` | `'node'` | `'node'` для бэкенда, `'jsdom'` для фронтенда |
| `aliases` | — | Алиасы путей (формат как в Vite) |
> Папку `coverage/` нужно добавить в `.gitignore` проекта.
### @vigdorov/playwright-config
E2E-тесты в трёх браузерах: Chromium, Firefox, WebKit. Автоматически адаптируется к CI-окружению.
```ts
// playwright.config.ts
import {playwrightConfig} from '@vigdorov/playwright-config';
export default playwrightConfig({
baseURL: 'http://localhost:5176',
});
```
| Параметр | По умолчанию | Описание |
|----------|-------------|----------|
| `baseURL` | — | URL приложения (обязательный) |
| `testDir` | `'e2e'` | Папка с тестами |
| `retries` | `0` (локально) / `2` (CI) | Количество ретраев |
### @vigdorov/knip-config
Поиск неиспользуемого кода: файлы, экспорты, зависимости. Завершается с ошибкой если находит проблемы — используй в check-скрипте для блокировки сборки.
```ts
// knip.config.ts
import {knipConfig} from '@vigdorov/knip-config';
export default knipConfig({
entry: ['src/main.tsx'],
});
```
| Параметр | По умолчанию | Описание |
|----------|-------------|----------|
| `entry` | `['src/index.ts']` | Точки входа |
| `project` | `['src/**/*.{ts,tsx,js,jsx}']` | Файлы проекта |
| `ignore` | `['**/*.test.*', '**/*.spec.*', 'e2e/**', '**/*.d.ts']` | Игнорируемые паттерны |
| `ignoreDependencies` | `[]` | Зависимости, которые не проверять |
---
## Рекомендуемые скрипты
```json
{
"scripts": {
"dev": "vite",
"build": "npm run check && vite build",
"check": "knip",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write src",
"test": "jest",
"e2e": "playwright test"
}
}
```

333
docs/requirements.md Normal file
View File

@ -0,0 +1,333 @@
# dev-configs — Требования
## Общее описание
Monorepo библиотека с общими конфигами для TypeScript-проектов. Публикуется как набор npm-пакетов в Gitea npm registry.
## Архитектура
- **Monorepo:** npm workspaces
- **Scope:** `@vigdorov/`
- **Registry:** Gitea npm (https://git.vigdorov.ru/api/packages/vigdorov/npm/)
- **Сборка пакетов:** tsc → dist/
- **Версионирование:** Semver, независимое, ручное (`npm version -w`)
- **Публикация:** Только пакеты с поднятой версией (CI сравнивает с registry)
- **CI:** Drone CI, тип `library` в ci-templates, trigger только на main
## Пакеты
### @vigdorov/prettier-config
**Тип:** Статичный объект (без функции-генератора)
```json
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": false,
"jsxSingleQuote": false,
"arrowParens": "always"
}
```
### @vigdorov/eslint-config
**Тип:** Функции-генераторы
**Формат:** ESLint 9+ flat config
**Пресеты:** `base`, `react`, `node`
**Зависимости:**
- eslint
- typescript-eslint (v8+)
- @stylistic/eslint-plugin
- eslint-plugin-unused-imports
- eslint-plugin-react (для react)
- eslint-plugin-react-hooks (для react)
**Правила — качество кода (base):**
- `eqeqeq: "error"`
- `no-console: ["warn", {allow: ["warn", "error"]}]`
- `no-alert: "warn"`
- `no-param-reassign: ["error", {props: true}]`
- `no-useless-concat: "warn"`
- `no-else-return: "warn"`
- `no-lonely-if: "warn"`
- `no-constructor-return: "warn"`
- `no-sequences: "warn"`
- `prefer-promise-reject-errors: "warn"`
- `require-await: "warn"`
- `no-new: "warn"`
- `no-multi-str: "warn"`
- `no-multi-assign: "warn"`
- `no-nested-ternary: "warn"`
- `no-useless-computed-key: "warn"`
- `no-useless-constructor: "warn"`
- `no-var: "warn"`
- `no-duplicate-imports: "warn"`
- `no-plusplus: "warn"`
- `no-bitwise: "warn"`
- `prefer-const: "warn"`
- `prefer-rest-params: "warn"`
- `prefer-template: "warn"`
- `array-callback-return: ["warn", {allowImplicit: true, checkForEach: true}]`
- `default-param-last: "warn"`
- `yoda: "warn"`
**Правила — TypeScript (base):**
- `@typescript-eslint/no-explicit-any: "error"`
- `@typescript-eslint/no-empty-object-type: "error"`
- `@typescript-eslint/no-unsafe-function-type: "error"`
- `@typescript-eslint/no-wrapper-object-types: "error"`
- `@typescript-eslint/no-use-before-define: "warn"`
- `@typescript-eslint/no-shadow: "warn"`
**Правила — unused imports (base):**
- `unused-imports/no-unused-imports: "warn"`
- `unused-imports/no-unused-vars: ["warn", {vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_"}]`
**Правила — @stylistic (base):**
- `@stylistic/no-multiple-empty-lines: ["warn", {max: 1}]`
- `@stylistic/lines-between-class-members: ["warn", "always"]`
- `@stylistic/line-comment-position: ["warn", {position: "above"}]`
- `@stylistic/multiline-comment-style: ["warn", "starred-block"]`
- `@stylistic/capitalized-comments: "warn"`
- `@stylistic/max-len: ["warn", {code: 120, ignoreComments: true, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true, ignoreRegExpLiterals: true}]`
**Правила — React (пресет react, поверх base):**
- `react/prop-types: "off"`
- `react/react-in-jsx-scope: "off"`
- `react/jsx-filename-extension: ["warn", {extensions: [".tsx"]}]`
- `react/jsx-props-no-spreading: "warn"`
- `react/jsx-key: "warn"`
- `react/no-array-index-key: "warn"`
- `react/destructuring-assignment: "warn"`
- `react/prefer-stateless-function: "warn"`
- `react/jsx-fragments: ["off", "element"]`
- `react-hooks/exhaustive-deps: "warn"`
**Правила — Node (пресет node):**
- Базовые правила из base
- React-специфичные правила отключены
### @vigdorov/typescript-config
**Тип:** JSON-файлы для extends (без сборки)
**base.json:**
```json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": false,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"allowSyntheticDefaultImports": true,
"removeComments": true,
"sourceMap": true,
"incremental": true,
"skipLibCheck": true,
"isolatedModules": true
}
}
```
**react.json:**
```json
{
"extends": "./base.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "react-jsx"
}
}
```
### @vigdorov/vite-config
**Тип:** Функции-генераторы
**Пресеты:** `spa`, `library`
**spa:**
```ts
import {spa} from '@vigdorov/vite-config';
export default spa({
port: 5176,
aliases: {'@': 'src'},
proxy: {'/api': 'http://localhost:3003'},
});
```
**library:**
```ts
import {library} from '@vigdorov/vite-config';
export default library({
entry: 'src/index.ts',
name: 'my-lib',
aliases: {'@': 'src'},
external: ['react', 'react-dom'],
formats: ['es', 'cjs'],
});
```
**Генератор spa:**
- Подключает @vitejs/plugin-react
- Преобразует aliases в resolve.alias с абсолютными путями
- Настраивает server.port и server.proxy
- Дефолты: base: "/", outDir: "dist"
**Генератор library:**
- Настраивает build.lib (entry, name, formats)
- rollupOptions.external — исключает peer-зависимости
- fileName: index.mjs / index.cjs
- Алиасы работают так же как в SPA
### @vigdorov/jest-config
**Тип:** Функция-генератор
```ts
import {jestConfig} from '@vigdorov/jest-config';
export default jestConfig({
environment: 'jsdom',
aliases: {'@': 'src'},
});
```
**Базовый конфиг:**
- clearMocks: true
- collectCoverage: true
- coverageReporters: ['html', 'text', 'text-summary', 'lcov']
- coverageDirectory: 'coverage'
- testMatch: ['**/__tests__/**/*.(j|t)s?(x)', '**/?(*.)+(spec|test).(j|t)s?(x)']
- testPathIgnorePatterns: ['/node_modules/', '/dist/']
- transform: @swc/jest
**Параметры:**
- environment: 'node' | 'jsdom'
- aliases: Record<string, string> — преобразуется в moduleNameMapper
### @vigdorov/playwright-config
**Тип:** Функция-генератор
```ts
import {playwrightConfig} from '@vigdorov/playwright-config';
export default playwrightConfig({
baseURL: 'http://localhost:5176',
testDir: 'e2e',
retries: 0,
});
```
**Базовый конфиг:**
- timeout: 30000
- retries: CI ? (params.retries ?? 2) : (params.retries ?? 0)
- reporter: CI ? 'html' : 'list'
- use.trace: 'on-first-retry'
- use.screenshot: 'only-on-failure'
- Три браузера: chromium, firefox, webkit
### @vigdorov/knip-config
**Тип:** Функция-генератор
```ts
import {knipConfig} from '@vigdorov/knip-config';
export default knipConfig({
entry: ['src/main.tsx'],
});
```
**Базовый конфиг:**
- entry: params.entry ?? ['src/index.ts']
- project: params.project ?? ['src/**/*.{ts,tsx,js,jsx}']
- ignore: ['**/*.test.*', '**/*.spec.*', 'e2e/**', '**/*.d.ts', ...params.ignore]
- ignoreDependencies: params.ignoreDependencies ?? []
- Knip завершается с exit code 1 при ошибках (используется в check-скрипте для блокировки сборки)
## CI/CD
### Тип `library` в ci-templates
**Отдельный `library.drone.yml`** (не base.drone.yml)
**Pipeline:** prepare → install → test → build → publish
**Скрипт `publish-lib.sh`:**
- Настраивает .npmrc с GITEA_TOKEN
- Для каждого пакета в packages/ сравнивает версию с registry
- Публикует только пакеты с новой версией
**Trigger:** только main, только push
### service.yaml для dev-configs
```yaml
service:
name: dev-configs
type: library
library:
scope: "@vigdorov"
registry: "https://git.vigdorov.ru/api/packages/vigdorov/npm/"
```
## Процесс работы с версиями
1. Разработка в feature-ветке
2. Merge в main
3. Поднятие версии: `npm version patch -w packages/eslint`
4. Commit + push: `git commit -am "release: eslint@1.0.1"`
5. CI публикует только пакеты с новой версией
## Потребители
В каждом проекте-потребителе:
**.npmrc:**
```ini
@vigdorov:registry=https://git.vigdorov.ru/api/packages/vigdorov/npm/
```
**.gitignore (добавить):**
```
coverage/
```
## Структура monorepo
```
dev-configs/
├── packages/
│ ├── eslint/ → @vigdorov/eslint-config
│ ├── prettier/ → @vigdorov/prettier-config
│ ├── typescript/ → @vigdorov/typescript-config
│ ├── vite/ → @vigdorov/vite-config
│ ├── jest/ → @vigdorov/jest-config
│ ├── playwright/ → @vigdorov/playwright-config
│ └── knip/ → @vigdorov/knip-config
├── package.json (workspace root, private: true)
├── tsconfig.base.json (общий tsconfig для сборки пакетов)
├── .npmrc
├── service.yaml
├── .drone.yml
├── .gitignore
└── CLAUDE.md
```

10026
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "dev-configs",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "npm run build -ws --if-present",
"test": "npm run test -ws --if-present",
"lint": "npm run lint -ws --if-present",
"check": "npm run check -ws --if-present"
}
}

View File

@ -0,0 +1,36 @@
{
"name": "@vigdorov/eslint-config",
"version": "1.0.0",
"description": "Shared ESLint configuration with flat config",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": "./dist/index.js",
"./base": "./dist/base.js",
"./react": "./dist/react.js",
"./node": "./dist/node.js"
},
"scripts": {
"build": "tsc"
},
"peerDependencies": {
"eslint": ">=9.0.0",
"typescript": ">=5.0.0"
},
"dependencies": {
"typescript-eslint": "^8.32.1",
"@stylistic/eslint-plugin": "^4.2.0",
"eslint-plugin-unused-imports": "^4.1.4"
},
"devDependencies": {
"eslint": "^9.27.0",
"typescript": "^5.8.3",
"@types/eslint": "^9.6.1"
},
"optionalDependencies": {
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0"
}
}

View File

@ -0,0 +1,30 @@
import tseslint from 'typescript-eslint';
import stylistic from '@stylistic/eslint-plugin';
import unusedImports from 'eslint-plugin-unused-imports';
import {qualityRules, typescriptRules, unusedImportsRules, stylisticRules} from './rules.js';
type ConfigArray = ReturnType<typeof tseslint.config>;
type ConfigOverride = (configs: ConfigArray) => ConfigArray;
export function base(override: ConfigOverride = (c) => c): ConfigArray {
const configs = tseslint.config(
...tseslint.configs.recommended,
{
plugins: {
'@stylistic': stylistic,
'unused-imports': unusedImports,
},
rules: {
...qualityRules,
...typescriptRules,
...unusedImportsRules,
...stylisticRules,
},
},
{
ignores: ['dist/', 'node_modules/', 'coverage/'],
},
);
return override(configs);
}

View File

@ -0,0 +1,3 @@
export {base} from './base.js';
export {react} from './react.js';
export {node} from './node.js';

View File

@ -0,0 +1,9 @@
import tseslint from 'typescript-eslint';
import {base} from './base.js';
type ConfigArray = ReturnType<typeof tseslint.config>;
type ConfigOverride = (configs: ConfigArray) => ConfigArray;
export function node(override: ConfigOverride = (c) => c): ConfigArray {
return base(override);
}

View File

@ -0,0 +1,33 @@
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import {base} from './base.js';
import {reactRules, reactHooksRules} from './rules.js';
type ConfigArray = ReturnType<typeof tseslint.config>;
type ConfigOverride = (configs: ConfigArray) => ConfigArray;
export function react(override: ConfigOverride = (c) => c): ConfigArray {
const baseConfigs = base();
const configs = tseslint.config(
...baseConfigs,
{
plugins: {
'react': reactPlugin,
'react-hooks': reactHooksPlugin,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...reactRules,
...reactHooksRules,
},
},
);
return override(configs);
}

View File

@ -0,0 +1,91 @@
import type {Linter} from 'eslint';
type Rules = Partial<Linter.RulesRecord>;
export const qualityRules: Rules = {
'eqeqeq': 'error',
'no-console': ['warn', {allow: ['warn', 'error']}],
'no-alert': 'warn',
'no-param-reassign': ['error', {props: true}],
'no-useless-concat': 'warn',
'no-else-return': 'warn',
'no-lonely-if': 'warn',
'no-constructor-return': 'warn',
'no-sequences': 'warn',
'prefer-promise-reject-errors': 'warn',
'require-await': 'warn',
'no-new': 'warn',
'no-multi-str': 'warn',
'no-multi-assign': 'warn',
'no-nested-ternary': 'warn',
'no-useless-computed-key': 'warn',
'no-useless-constructor': 'warn',
'no-var': 'warn',
'no-duplicate-imports': 'warn',
'no-plusplus': 'warn',
'no-bitwise': 'warn',
'prefer-const': 'warn',
'prefer-rest-params': 'warn',
'prefer-template': 'warn',
'array-callback-return': ['warn', {allowImplicit: true, checkForEach: true}],
'default-param-last': 'warn',
'yoda': 'warn',
};
export const typescriptRules: Rules = {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-empty-object-type': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-wrapper-object-types': 'error',
'@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-shadow': 'warn',
'no-shadow': 'off',
};
export const unusedImportsRules: Rules = {
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
};
export const stylisticRules: Rules = {
'@stylistic/no-multiple-empty-lines': ['warn', {max: 1}],
'@stylistic/lines-between-class-members': ['warn', 'always'],
'@stylistic/line-comment-position': ['warn', {position: 'above'}],
'@stylistic/multiline-comment-style': ['warn', 'starred-block'],
'@stylistic/capitalized-comments': 'warn',
'@stylistic/max-len': [
'warn',
{
code: 120,
ignoreComments: true,
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true,
},
],
};
export const reactRules: Rules = {
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-filename-extension': ['warn', {extensions: ['.tsx']}],
'react/jsx-props-no-spreading': 'warn',
'react/jsx-key': 'warn',
'react/no-array-index-key': 'warn',
'react/destructuring-assignment': 'warn',
'react/prefer-stateless-function': 'warn',
'react/jsx-fragments': ['off', 'element'],
};
export const reactHooksRules: Rules = {
'react-hooks/exhaustive-deps': 'warn',
};

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

View File

@ -0,0 +1,23 @@
{
"name": "@vigdorov/jest-config",
"version": "1.0.0",
"description": "Shared Jest configuration",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc"
},
"peerDependencies": {
"jest": ">=29.0.0"
},
"dependencies": {
"@swc/jest": "^0.2.38"
},
"devDependencies": {
"jest": "^29.7.0",
"@types/jest": "^29.5.14",
"typescript": "^5.8.3"
}
}

View File

@ -0,0 +1,28 @@
export interface JestConfigOptions {
environment?: 'node' | 'jsdom';
aliases?: Record<string, string>;
}
function resolveAliases(aliases: Record<string, string>): Record<string, string> {
return Object.fromEntries(
Object.entries(aliases).map(([key, aliasPath]) => [`^${key}(.*)$`, `<rootDir>/${aliasPath}$1`]),
);
}
export function jestConfig(options: JestConfigOptions = {}) {
const {environment = 'node', aliases} = options;
return {
testEnvironment: environment === 'jsdom' ? 'jsdom' : 'node',
clearMocks: true,
collectCoverage: true,
coverageReporters: ['html', 'text', 'text-summary', 'lcov'],
coverageDirectory: 'coverage',
testMatch: ['**/__tests__/**/*.(j|t)s?(x)', '**/?(*.)+(spec|test).(j|t)s?(x)'],
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
...(aliases ? {moduleNameMapper: resolveAliases(aliases)} : {}),
};
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

View File

@ -0,0 +1,19 @@
{
"name": "@vigdorov/knip-config",
"version": "1.0.0",
"description": "Shared Knip configuration",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc"
},
"peerDependencies": {
"knip": ">=5.0.0"
},
"devDependencies": {
"knip": "^5.51.0",
"typescript": "^5.8.3"
}
}

View File

@ -0,0 +1,22 @@
export interface KnipConfigOptions {
entry?: string[];
project?: string[];
ignore?: string[];
ignoreDependencies?: string[];
}
export function knipConfig(options: KnipConfigOptions = {}) {
const {
entry = ['src/index.ts'],
project = ['src/**/*.{ts,tsx,js,jsx}'],
ignore = [],
ignoreDependencies = [],
} = options;
return {
entry,
project,
ignore: ['**/*.test.*', '**/*.spec.*', 'e2e/**', '**/*.d.ts', ...ignore],
ignoreDependencies,
};
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

View File

@ -0,0 +1,19 @@
{
"name": "@vigdorov/playwright-config",
"version": "1.0.0",
"description": "Shared Playwright configuration",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc"
},
"peerDependencies": {
"@playwright/test": ">=1.40.0"
},
"devDependencies": {
"@playwright/test": "^1.52.0",
"typescript": "^5.8.3"
}
}

View File

@ -0,0 +1,29 @@
import type {PlaywrightTestConfig} from '@playwright/test';
export interface PlaywrightConfigOptions {
baseURL: string;
testDir?: string;
retries?: number;
}
export function playwrightConfig(options: PlaywrightConfigOptions): PlaywrightTestConfig {
const {baseURL, testDir = 'e2e', retries} = options;
const isCI = !!process.env.CI;
return {
testDir,
timeout: 30_000,
retries: retries ?? (isCI ? 2 : 0),
reporter: isCI ? 'html' : 'list',
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{name: 'chromium', use: {browserName: 'chromium'}},
{name: 'firefox', use: {browserName: 'firefox'}},
{name: 'webkit', use: {browserName: 'webkit'}},
],
};
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

View File

@ -0,0 +1,19 @@
{
"name": "@vigdorov/prettier-config",
"version": "1.0.0",
"description": "Shared Prettier configuration",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc"
},
"peerDependencies": {
"prettier": ">=3.0.0"
},
"devDependencies": {
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}
}

View File

@ -0,0 +1,15 @@
import type {Config} from 'prettier';
const config: Config = {
printWidth: 120,
tabWidth: 4,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: false,
jsxSingleQuote: false,
arrowParens: 'always',
};
export default config;

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": false,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"allowSyntheticDefaultImports": true,
"removeComments": true,
"sourceMap": true,
"incremental": true,
"skipLibCheck": true,
"isolatedModules": true
}
}

View File

@ -0,0 +1,11 @@
{
"name": "@vigdorov/typescript-config",
"version": "1.0.0",
"description": "Shared TypeScript configurations",
"type": "module",
"files": ["base.json", "react.json"],
"exports": {
"./base": "./base.json",
"./react": "./react.json"
}
}

View File

@ -0,0 +1,7 @@
{
"extends": "./base.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "react-jsx"
}
}

View File

@ -0,0 +1,27 @@
{
"name": "@vigdorov/vite-config",
"version": "1.0.0",
"description": "Shared Vite configuration",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": "./dist/index.js",
"./spa": "./dist/spa.js",
"./library": "./dist/library.js"
},
"scripts": {
"build": "tsc"
},
"peerDependencies": {
"vite": ">=6.0.0"
},
"dependencies": {
"@vitejs/plugin-react": "^4.5.2"
},
"devDependencies": {
"vite": "^6.3.5",
"typescript": "^5.8.3"
}
}

View File

@ -0,0 +1,2 @@
export {spa, type SpaOptions} from './spa.js';
export {library, type LibraryOptions} from './library.js';

View File

@ -0,0 +1,31 @@
import {defineConfig, type UserConfig} from 'vite';
import {resolveAliases} from './utils.js';
export interface LibraryOptions {
entry: string;
name: string;
aliases?: Record<string, string>;
external?: string[];
formats?: ('es' | 'cjs')[];
}
export function library(options: LibraryOptions): UserConfig {
const {entry, name, aliases, external = [], formats = ['es', 'cjs']} = options;
return defineConfig({
resolve: {
alias: resolveAliases(aliases),
},
build: {
lib: {
entry,
name,
formats,
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
external,
},
},
});
}

30
packages/vite/src/spa.ts Normal file
View File

@ -0,0 +1,30 @@
import react from '@vitejs/plugin-react';
import {defineConfig, type UserConfig} from 'vite';
import {resolveAliases} from './utils.js';
export interface SpaOptions {
port?: number;
aliases?: Record<string, string>;
proxy?: Record<string, string>;
base?: string;
outDir?: string;
}
export function spa(options: SpaOptions = {}): UserConfig {
const {port = 5173, aliases, proxy, base = '/', outDir = 'dist'} = options;
return defineConfig({
plugins: [react()],
base,
resolve: {
alias: resolveAliases(aliases),
},
server: {
port,
proxy,
},
build: {
outDir,
},
});
}

View File

@ -0,0 +1,9 @@
import path from 'node:path';
export function resolveAliases(aliases: Record<string, string> | undefined): Record<string, string> {
if (!aliases) return {};
return Object.fromEntries(
Object.entries(aliases).map(([key, value]) => [key, path.resolve(process.cwd(), value)]),
);
}

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

7
service.yaml Normal file
View File

@ -0,0 +1,7 @@
service:
name: dev-configs
type: library
library:
scope: "@vigdorov"
registry: "https://git.vigdorov.ru/api/packages/vigdorov/npm/"

19
tsconfig.base.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true
}
}