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

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"]
}