chore(configs): migrate oxlint→eslint presets, refactor tsconfig

- Replace @robonen/oxlint with @robonen/eslint (composable ESLint flat-config
  presets: base, typescript, vue, vitest, imports, node, stylistic).
- Plugins bundled as deps: typescript-eslint, eslint-plugin-vue,
  @vitest/eslint-plugin, eslint-plugin-import-x, eslint-plugin-n,
  eslint-plugin-unicorn, @stylistic/eslint-plugin.
- @robonen/tsconfig: add base/dom/node/vue configs for composite project refs.
This commit is contained in:
2026-06-07 16:28:10 +07:00
parent 1d3efa5028
commit 7693b49253
51 changed files with 988 additions and 916 deletions
+74
View File
@@ -0,0 +1,74 @@
# @robonen/eslint
Composable [ESLint](https://eslint.org) flat-config presets.
## Install
```bash
pnpm install -D @robonen/eslint eslint jiti
```
> `jiti` lets ESLint load a TypeScript `eslint.config.ts`.
## Usage
Create `eslint.config.ts` in your project root:
```ts
import { compose, base, typescript, vue, vitest, imports } from '@robonen/eslint';
export default compose(base, typescript, vue, vitest, imports);
```
Append custom config objects after presets to override them:
```ts
import { compose, base, typescript } from '@robonen/eslint';
export default compose(base, typescript, {
rules: { 'no-console': 'off' },
}, {
files: ['**/*.vue'],
rules: { '@stylistic/no-multiple-empty-lines': 'off' },
});
```
## Presets
| Preset | Plugin(s) | Description |
| ------------ | -------------------------------------- | ---------------------------------------------- |
| `base` | `@eslint/js`, `eslint-plugin-unicorn` | Core eslint + unicorn rules, global ignores |
| `typescript` | `typescript-eslint` | TypeScript rules (`**/*.ts`, `**/*.vue`) |
| `vue` | `eslint-plugin-vue` | Vue 3 Composition API / `<script setup>` rules |
| `vitest` | `@vitest/eslint-plugin` | Test file rules |
| `imports` | `eslint-plugin-import-x` | Import rules (cycles, duplicates, ordering) |
| `node` | `eslint-plugin-n` | Node.js-specific rules |
| `stylistic` | `@stylistic/eslint-plugin` | Formatting rules |
`ignores` is also exported on its own if you only want the global ignore list.
## Migrating from `@robonen/oxlint`
This package replaces `@robonen/oxlint`. The preset names and intent are
preserved, with these mapping notes:
- `eslint/*` rules → ESLint core rules (no prefix), e.g. `eslint/no-console``no-console`.
- `typescript/*``@typescript-eslint/*`.
- `import/*``import-x/*` (via `eslint-plugin-import-x`).
- `node/*``n/*` (via `eslint-plugin-n`).
- `@stylistic/*` and `unicorn/*` are unchanged.
- `oxc/*` rules are oxc-exclusive and have **no ESLint equivalent**; they are
dropped. Their intent is largely covered by `@eslint/js` recommended and
`unicorn`.
- `categories`/`env`/`ignorePatterns` (oxlint config keys) are replaced by flat
config equivalents: `@eslint/js` recommended, `languageOptions.globals`, and
the `ignores` preset.
## API
### `compose(...configs): FlatConfigArray`
Flattens presets (arrays) and inline overrides (single objects) into one ordered
flat config array. Later entries override earlier ones — ESLint flat-config
semantics. Falsy entries (`false`/`null`/`undefined`) are skipped, enabling
conditional composition.
+3
View File
@@ -0,0 +1,3 @@
import { base, compose, imports, stylistic, typescript } from './src';
export default compose(base, typescript, imports, stylistic);
+73
View File
@@ -0,0 +1,73 @@
{
"name": "@robonen/eslint",
"version": "0.0.1",
"license": "Apache-2.0",
"description": "Composable ESLint flat configuration presets",
"keywords": [
"eslint",
"eslint-config",
"flat-config",
"linter",
"config",
"presets"
],
"author": "Robonen Andrew <robonenandrew@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/robonen/tools.git",
"directory": "configs/eslint"
},
"packageManager": "pnpm@10.30.3",
"engines": {
"node": ">=24.13.1"
},
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"scripts": {
"lint:check": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run",
"dev": "vitest dev",
"build": "tsdown"
},
"dependencies": {
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "catalog:",
"@vitest/eslint-plugin": "^1.6.19",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-n": "^18.0.1",
"eslint-plugin-unicorn": "^64.0.0",
"eslint-plugin-vue": "^10.9.2",
"globals": "^17.6.0",
"jiti": "^2.7.0",
"typescript-eslint": "^8.60.1",
"vue-eslint-parser": "^10.4.1"
},
"devDependencies": {
"@robonen/eslint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@robonen/tsdown": "workspace:*",
"eslint": "catalog:",
"tsdown": "catalog:"
},
"peerDependencies": {
"eslint": ">=9.0.0"
},
"publishConfig": {
"access": "public"
}
}
+21
View File
@@ -0,0 +1,21 @@
# Rules Reference
Документация по preset-ам `@robonen/eslint`: что включает каждый preset и какие правила чаще всего влияют на код.
## Presets
- [base](./base.md)
- [typescript](./typescript.md)
- [vue](./vue.md)
- [vitest](./vitest.md)
- [imports](./imports.md)
- [node](./node.md)
- [stylistic](./stylistic.md)
## Как читать
- `Purpose` — зачем preset подключать.
- `Key Rules` — ключевые правила из preset-а (не полный dump).
- `Examples` — минимальные `good/bad` примеры.
Для точного источника правил см. файлы в `configs/eslint/src/presets/*.ts`.
+36
View File
@@ -0,0 +1,36 @@
# base preset
## Purpose
Базовый quality-профиль для JS/TS-проектов: корректность, анти-паттерны, безопасные дефолты. Включает `@eslint/js` recommended (аналог oxlint-категории `correctness`) + правила `unicorn`.
## Key Rules
- `eqeqeq`: запрещает `==`, требует `===`.
- `no-unused-vars`: запрещает неиспользуемые переменные (кроме `_name`).
- `no-eval`, `no-var`, `prefer-const`.
- `unicorn/prefer-node-protocol`: требует `node:` для built-in модулей.
- `unicorn/no-thenable`: запрещает thenable-объекты.
> Правила `oxc/*` из `@robonen/oxlint` не имеют аналога в ESLint и были убраны;
> их назначение покрывается `@eslint/js` recommended и `unicorn`.
## Examples
```ts
// ✅ Good
import { readFile } from 'node:fs/promises';
const id = 42;
if (id === 42) {
throw new Error('unexpected');
}
// ❌ Bad
import { readFile } from 'fs/promises';
var id = 42;
if (id == '42') {
throw 'unexpected';
}
```
+27
View File
@@ -0,0 +1,27 @@
# imports preset
## Purpose
Чистые границы модулей и предсказуемые импорты (через `eslint-plugin-import-x`).
## Key Rules
- `import-x/no-duplicates`.
- `import-x/no-self-import`.
- `import-x/no-cycle` (warn).
- `import-x/no-mutable-exports`.
- `import-x/consistent-type-specifier-style`: `prefer-top-level`.
## Examples
```ts
// ✅ Good
import type { User } from './types';
import { getUser } from './service';
// ❌ Bad
import { getUser } from './service';
import { getUser as getUser2 } from './service';
export let state = 0;
```
+22
View File
@@ -0,0 +1,22 @@
# node preset
## Purpose
Node.js-правила (через `eslint-plugin-n`) и Node-глобалы в `languageOptions.globals`.
## Key Rules
- `n/no-exports-assign`: запрещает перезапись `exports`.
- `n/no-new-require`: запрещает `new require(...)`.
## Examples
```ts
// ✅ Good
module.exports = { run };
const mod = require('./mod');
// ❌ Bad
exports = { run };
const bad = new require('./mod');
```
+51
View File
@@ -0,0 +1,51 @@
# stylistic preset
## Purpose
Форматирование через `@stylistic/eslint-plugin` (отступы, пробелы, скобки, переносы, TS/JSX-стиль).
## Defaults
- `indent: 2`
- `quotes: single`
- `semi: always`
- `braceStyle: stroustrup`
- `commaDangle: always-multiline`
- `arrowParens: as-needed`
## Key Rules
- `@stylistic/indent`, `@stylistic/no-tabs`.
- `@stylistic/quotes`, `@stylistic/semi`.
- `@stylistic/object-curly-spacing`, `@stylistic/comma-spacing`.
- `@stylistic/arrow-spacing`, `@stylistic/space-before-function-paren`.
- `@stylistic/max-statements-per-line`.
- `@stylistic/no-mixed-operators`.
- `@stylistic/member-delimiter-style` (TS).
## Examples
```ts
// ✅ Good
type User = {
id: string;
role: 'admin' | 'user';
};
const value = condition
? 'yes'
: 'no';
const sum = (a: number, b: number) => a + b;
// ❌ Bad
type User = {
id: string
role: 'admin' | 'user'
}
const value = condition ? 'yes' : 'no'; const x = 1;
const sum=(a:number,b:number)=>{ return a+b };
```
Полный список правил и их настройки см. в `src/presets/stylistic.ts`.
+33
View File
@@ -0,0 +1,33 @@
# typescript preset
## Purpose
TypeScript-правила для `.ts/.tsx/.mts/.cts` и `<script lang="ts">` в `.vue`. Базируется на `typescript-eslint` recommended (без type-checking).
## Key Rules
- `@typescript-eslint/consistent-type-imports`: выносит типы в `import type`.
- `@typescript-eslint/no-import-type-side-effects`: запрещает сайд-эффекты в type import.
- `@typescript-eslint/prefer-as-const`.
- `@typescript-eslint/no-namespace`, `@typescript-eslint/triple-slash-reference`.
- `@typescript-eslint/no-wrapper-object-types`: запрещает `String`, `Number`, `Boolean`.
## Examples
```ts
// ✅ Good
import type { User } from './types';
const status = 'ok' as const;
interface Payload {
value: string;
}
// ❌ Bad
import { User } from './types';
type Boxed = String;
namespace Legacy {
export const x = 1;
}
```
+34
View File
@@ -0,0 +1,34 @@
# vitest preset
## Purpose
Правила для тестов (`*.test.*`, `*.spec.*`, `test/**`, `__tests__/**`).
## Key Rules
- `vitest/no-conditional-tests`.
- `vitest/no-import-node-test`.
- `vitest/prefer-to-be-truthy`, `vitest/prefer-to-be-falsy`.
- `vitest/prefer-to-have-length`.
- Relaxations: `no-unused-vars` и `@typescript-eslint/no-explicit-any` выключены для тестов.
## Examples
```ts
// ✅ Good
import { describe, it, expect } from 'vitest';
describe('list', () => {
it('has items', () => {
expect([1, 2, 3]).toHaveLength(3);
expect(true).toBeTruthy();
});
});
// ❌ Bad
if (process.env.CI) {
it('conditionally runs', () => {
expect(true).toBe(true);
});
}
```
+30
View File
@@ -0,0 +1,30 @@
# vue preset
## Purpose
Правила для Vue 3 с упором на Composition API и `<script setup>`.
## Key Rules
- `vue/no-export-in-script-setup`.
- `vue/no-import-compiler-macros`.
- `vue/define-props-declaration`: type-based.
- `vue/define-emits-declaration`: type-based.
- `vue/valid-define-props`, `vue/valid-define-emits`.
- `vue/no-lifecycle-after-await`.
## Examples
```vue
<script setup lang="ts">
const props = defineProps<{ id: string }>();
const emit = defineEmits<{ change: [value: string] }>();
</script>
<!-- Bad -->
<script setup lang="ts">
import { defineProps } from 'vue';
export const x = 1;
const props = defineProps({ id: String });
</script>
```
+34
View File
@@ -0,0 +1,34 @@
import type { FlatConfigArray, FlatConfigInput } from './types';
/**
* Compose multiple ESLint flat configurations into a single flat config array.
*
* ESLint flat config is an ordered array where later entries override earlier
* ones, so composition is a flatten: each preset (an array) and each inline
* override (a single object) are concatenated in order. `undefined`/`null`
* inputs are skipped, allowing conditional spreads.
*
* @example
* ```ts
* import { compose, base, typescript, vue } from '@robonen/eslint';
*
* export default compose(base, typescript, vue, {
* rules: { 'no-console': 'off' },
* });
* ```
*/
export function compose(...configs: Array<FlatConfigInput | false | null | undefined>): FlatConfigArray {
const result: FlatConfigArray = [];
for (const config of configs) {
if (!config)
continue;
if (Array.isArray(config))
result.push(...config);
else
result.push(config);
}
return result;
}
+13
View File
@@ -0,0 +1,13 @@
/* Compose */
export { compose } from './compose';
/* Presets */
export { base, ignores, typescript, vue, vitest, imports, node, stylistic } from './presets';
/* Types */
export type {
FlatConfig,
FlatConfigArray,
FlatConfigInput,
Rules,
} from './types';
+101
View File
@@ -0,0 +1,101 @@
import type { FlatConfigArray } from '../types';
import js from '@eslint/js';
import unicorn from 'eslint-plugin-unicorn';
import globals from 'globals';
/**
* Globally ignored paths — build output, coverage, generated artifacts.
*
* A flat config entry with only `ignores` acts as a global ignore.
*/
export const ignores: FlatConfigArray = [
{
name: 'robonen/ignores',
ignores: [
'**/dist/**',
'**/coverage/**',
'**/node_modules/**',
'**/.nuxt/**',
'**/.output/**',
'**/storybook-static/**',
'**/*.min.*',
],
},
];
/**
* Base configuration for any JavaScript/TypeScript project.
*
* Includes `@eslint/js` recommended rules (the analog of oxlint's
* `correctness` category) plus opinionated `eslint` core and `unicorn` rules.
*
* > Note: oxlint's `oxc/*` rules have no ESLint equivalent and are dropped in
* > this migration; their intent is largely covered by `@eslint/js` recommended
* > and `unicorn`.
*/
export const base: FlatConfigArray = [
...ignores,
{
name: 'robonen/base/setup',
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: {
...globals.es2024,
...globals.browser,
...globals.node,
},
},
},
js.configs.recommended,
{
name: 'robonen/base/rules',
plugins: {
unicorn,
},
rules: {
/* ── eslint core ──────────────────────────────────────── */
eqeqeq: 'error',
'no-console': 'warn',
'no-debugger': 'error',
'no-eval': 'error',
'no-var': 'error',
'prefer-const': 'error',
'prefer-template': 'warn',
'no-useless-constructor': 'warn',
'no-useless-rename': 'warn',
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'no-self-compare': 'error',
'no-template-curly-in-string': 'warn',
'no-throw-literal': 'error',
'no-return-assign': 'warn',
'no-else-return': 'warn',
'no-lonely-if': 'warn',
'no-unneeded-ternary': 'warn',
'prefer-object-spread': 'warn',
'prefer-exponentiation-operator': 'warn',
'no-useless-computed-key': 'warn',
'no-useless-concat': 'warn',
curly: 'off',
/* ── unicorn ──────────────────────────────────────────── */
'unicorn/prefer-node-protocol': 'error',
'unicorn/no-instanceof-builtins': 'error',
'unicorn/no-new-array': 'error',
'unicorn/prefer-array-flat-map': 'warn',
'unicorn/prefer-array-flat': 'warn',
'unicorn/prefer-includes': 'warn',
'unicorn/prefer-string-slice': 'warn',
'unicorn/prefer-string-starts-ends-with': 'warn',
'unicorn/throw-new-error': 'error',
'unicorn/error-message': 'warn',
'unicorn/no-useless-spread': 'warn',
'unicorn/no-useless-undefined': 'off',
'unicorn/prefer-optional-catch-binding': 'warn',
'unicorn/prefer-type-error': 'warn',
'unicorn/no-thenable': 'error',
'unicorn/prefer-number-properties': 'warn',
'unicorn/prefer-global-this': 'warn',
},
},
];
+53
View File
@@ -0,0 +1,53 @@
import type { FlatConfigArray } from '../types';
import importX, { createNodeResolver } from 'eslint-plugin-import-x';
/**
* Import configuration for clean module boundaries.
*
* Uses `eslint-plugin-import-x` (the faster, modern fork) under the `import-x`
* namespace, plus the core `sort-imports` rule. The Node resolver is configured
* explicitly (flat config no longer ships a default) with TypeScript/Vue-aware
* extensions so resolution-based rules (`no-cycle`, `no-self-import`) work.
*/
export const imports: FlatConfigArray = [
{
name: 'robonen/imports',
plugins: {
'import-x': importX,
},
settings: {
'import-x/resolver-next': [
createNodeResolver({
extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts', '.vue', '.json', '.node'],
}),
],
},
rules: {
'import-x/no-duplicates': 'error',
'import-x/no-self-import': 'error',
'import-x/no-cycle': 'warn',
'import-x/first': 'warn',
'import-x/no-mutable-exports': 'error',
'import-x/no-amd': 'error',
'import-x/no-commonjs': 'warn',
'import-x/no-empty-named-blocks': 'warn',
'import-x/consistent-type-specifier-style': ['warn', 'prefer-top-level'],
/* Only enforce member order within `{ … }`; declaration order is sorted
by source path across the codebase, which core `sort-imports` (orders
by first member name) would otherwise fight. */
'sort-imports': ['warn', { ignoreDeclarationSort: true }],
},
},
{
/* Vue SFCs may have two <script> blocks (`<script>` + `<script setup>`),
which the parser concatenates — `import-x/first` then wrongly flags the
second block's imports as out of place. Kept here (rather than in the
`vue` preset) so it wins regardless of preset composition order. */
name: 'robonen/imports/vue',
files: ['**/*.vue'],
rules: {
'import-x/first': 'off',
},
},
];
+7
View File
@@ -0,0 +1,7 @@
export { base, ignores } from './base';
export { typescript } from './typescript';
export { vue } from './vue';
export { vitest } from './vitest';
export { imports } from './imports';
export { node } from './node';
export { stylistic } from './stylistic';
+27
View File
@@ -0,0 +1,27 @@
import type { FlatConfigArray } from '../types';
import nodePlugin from 'eslint-plugin-n';
import globals from 'globals';
/**
* Node.js-specific configuration.
*
* Registers `eslint-plugin-n` (the maintained successor of `eslint-plugin-node`)
* under the `n` namespace and adds Node globals.
*/
export const node: FlatConfigArray = [
{
name: 'robonen/node',
plugins: {
n: nodePlugin,
},
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'n/no-exports-assign': 'error',
'n/no-new-require': 'error',
},
},
];
+165
View File
@@ -0,0 +1,165 @@
import type { FlatConfigArray } from '../types';
import stylisticPlugin from '@stylistic/eslint-plugin';
/**
* Stylistic formatting rules via `@stylistic/eslint-plugin`.
*
* Roughly equivalent to the plugin's `customize()` defaults:
* - indent: 2
* - quotes: single
* - semi: true
* - braceStyle: stroustrup
* - commaDangle: always-multiline
* - arrowParens: as-needed
* - blockSpacing: true
* - quoteProps: consistent-as-needed
* - jsx: true
*
* @see https://eslint.style/guide/config-presets
*/
export const stylistic: FlatConfigArray = [
{
name: 'robonen/stylistic',
plugins: {
'@stylistic': stylisticPlugin,
},
rules: {
/* ── spacing & layout ─────────────────────────────────── */
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/arrow-spacing': ['error', { after: true, before: true }],
'@stylistic/block-spacing': ['error', 'always'],
'@stylistic/comma-spacing': ['error', { after: true, before: false }],
'@stylistic/computed-property-spacing': ['error', 'never', { enforceForClassMembers: true }],
'@stylistic/dot-location': ['error', 'property'],
'@stylistic/key-spacing': ['error', { afterColon: true, beforeColon: false }],
'@stylistic/keyword-spacing': ['error', { after: true, before: true }],
'@stylistic/no-mixed-spaces-and-tabs': 'error',
'@stylistic/no-multi-spaces': 'error',
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/no-whitespace-before-property': 'error',
'@stylistic/rest-spread-spacing': ['error', 'never'],
'@stylistic/semi-spacing': ['error', { after: true, before: false }],
'@stylistic/space-before-blocks': ['error', 'always'],
'@stylistic/space-before-function-paren': ['error', { anonymous: 'always', asyncArrow: 'always', named: 'never' }],
'@stylistic/space-in-parens': ['error', 'never'],
'@stylistic/space-infix-ops': 'error',
'@stylistic/space-unary-ops': ['error', { nonwords: false, words: true }],
'@stylistic/template-curly-spacing': 'error',
'@stylistic/template-tag-spacing': ['error', 'never'],
/* ── braces & blocks ──────────────────────────────────── */
'@stylistic/brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
'@stylistic/arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'@stylistic/no-extra-parens': ['error', 'functions'],
'@stylistic/no-floating-decimal': 'error',
'@stylistic/wrap-iife': ['error', 'any', { functionPrototypeMethods: true }],
'@stylistic/new-parens': 'error',
'@stylistic/padded-blocks': ['error', { blocks: 'never', classes: 'never', switches: 'never' }],
/* ── punctuation ──────────────────────────────────────── */
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/comma-style': ['error', 'last'],
'@stylistic/semi': ['error', 'always'],
'@stylistic/quotes': ['error', 'single', { allowTemplateLiterals: 'always', avoidEscape: false }],
'@stylistic/quote-props': ['error', 'as-needed'],
/* ── indentation ──────────────────────────────────────── */
'@stylistic/indent': ['error', 2, {
ArrayExpression: 1,
CallExpression: { arguments: 1 },
flatTernaryExpressions: false,
FunctionDeclaration: { body: 1, parameters: 1, returnType: 1 },
FunctionExpression: { body: 1, parameters: 1, returnType: 1 },
ignoreComments: false,
ignoredNodes: [
'TSUnionType',
'TSIntersectionType',
],
ImportDeclaration: 1,
MemberExpression: 1,
ObjectExpression: 1,
offsetTernaryExpressions: true,
outerIIFEBody: 1,
SwitchCase: 1,
tabLength: 2,
VariableDeclarator: 1,
}],
'@stylistic/indent-binary-ops': ['error', 2],
'@stylistic/no-tabs': 'error',
/* ── line breaks ──────────────────────────────────────── */
'@stylistic/eol-last': 'error',
'@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }],
'@stylistic/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
'@stylistic/max-statements-per-line': ['error', { max: 1 }],
'@stylistic/multiline-ternary': ['error', 'always-multiline'],
'@stylistic/operator-linebreak': ['error', 'before'],
'@stylistic/object-curly-spacing': ['error', 'always'],
/* ── generators ───────────────────────────────────────── */
'@stylistic/generator-star-spacing': ['error', { after: true, before: false }],
'@stylistic/yield-star-spacing': ['error', { after: true, before: false }],
/* ── operators & mixed ────────────────────────────────── */
'@stylistic/no-mixed-operators': ['error', {
allowSamePrecedence: true,
groups: [
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof'],
],
}],
/* ── typescript styling ───────────────────────────────── */
'@stylistic/member-delimiter-style': ['error', {
multiline: { delimiter: 'semi', requireLast: true },
multilineDetection: 'brackets',
overrides: {
interface: {
multiline: { delimiter: 'semi', requireLast: true },
},
},
singleline: { delimiter: 'semi' },
}],
'@stylistic/type-annotation-spacing': ['error', {}],
'@stylistic/type-generic-spacing': 'error',
'@stylistic/type-named-tuple-spacing': 'error',
/* ── comments ─────────────────────────────────────────── */
'@stylistic/spaced-comment': ['error', 'always', {
block: { balanced: true, exceptions: ['*'], markers: ['!'] },
line: { exceptions: ['/', '#'], markers: ['/'] },
}],
/* ── jsx ───────────────────────────────────────────────── */
'@stylistic/jsx-closing-bracket-location': 'error',
'@stylistic/jsx-closing-tag-location': 'error',
'@stylistic/jsx-curly-brace-presence': ['error', { propElementValues: 'always' }],
'@stylistic/jsx-curly-newline': 'error',
'@stylistic/jsx-curly-spacing': ['error', 'never'],
'@stylistic/jsx-equals-spacing': 'error',
'@stylistic/jsx-first-prop-new-line': 'error',
'@stylistic/jsx-function-call-newline': ['error', 'multiline'],
'@stylistic/jsx-indent-props': ['error', 2],
'@stylistic/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }],
'@stylistic/jsx-one-expression-per-line': ['error', { allow: 'single-child' }],
'@stylistic/jsx-quotes': 'error',
'@stylistic/jsx-tag-spacing': ['error', {
afterOpening: 'never',
beforeClosing: 'never',
beforeSelfClosing: 'always',
closingSlash: 'never',
}],
'@stylistic/jsx-wrap-multilines': ['error', {
arrow: 'parens-new-line',
assignment: 'parens-new-line',
condition: 'parens-new-line',
declaration: 'parens-new-line',
logical: 'parens-new-line',
prop: 'parens-new-line',
propertyValue: 'parens-new-line',
return: 'parens-new-line',
}],
},
},
];
+51
View File
@@ -0,0 +1,51 @@
import type { FlatConfigArray } from '../types';
import tseslint from 'typescript-eslint';
/**
* TypeScript-specific configuration.
*
* Pulls in `typescript-eslint`'s recommended (non type-checked) setup — which
* registers the parser/plugin and disables core rules superseded by their
* TypeScript-aware counterparts — then layers opinionated rules on top.
*
* `.vue` files are included so the rules apply inside `<script lang="ts">`
* blocks; the `vue` preset assigns the matching parser for them.
*/
export const typescript: FlatConfigArray = [
...tseslint.configs.recommended,
{
name: 'robonen/typescript',
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.vue'],
rules: {
/* core no-unused-vars is replaced by the TS-aware version */
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
/* TypeScript already reports undefined names; `no-undef` only adds
false positives (e.g. globals, auto-imports, compiler macros). */
'no-undef': 'off',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/no-empty-object-type': ['warn', { allowInterfaces: 'with-single-extends' }],
'@typescript-eslint/no-wrapper-object-types': 'error',
'@typescript-eslint/no-duplicate-enum-values': 'error',
'@typescript-eslint/no-unsafe-declaration-merging': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/no-useless-empty-export': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'@typescript-eslint/prefer-function-type': 'warn',
'@typescript-eslint/ban-tslint-comment': 'error',
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/no-unnecessary-type-constraint': 'warn',
'@typescript-eslint/adjacent-overload-signatures': 'warn',
'@typescript-eslint/array-type': ['warn', { default: 'array-simple' }],
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/triple-slash-reference': 'error',
'@typescript-eslint/no-namespace': 'error',
},
},
];
+38
View File
@@ -0,0 +1,38 @@
import type { FlatConfigArray } from '../types';
import vitestPlugin from '@vitest/eslint-plugin';
/**
* Vitest configuration for test files.
*
* Scoped to common test file patterns; also relaxes a few strict rules that
* are noisy in tests.
*/
export const vitest: FlatConfigArray = [
{
name: 'robonen/vitest',
files: [
'**/*.test.{ts,tsx,js,jsx}',
'**/*.spec.{ts,tsx,js,jsx}',
'**/test/**/*.{ts,tsx,js,jsx}',
'**/__tests__/**/*.{ts,tsx,js,jsx}',
],
plugins: {
vitest: vitestPlugin,
},
rules: {
'vitest/no-conditional-tests': 'warn',
'vitest/no-import-node-test': 'error',
'vitest/prefer-to-be-truthy': 'warn',
'vitest/prefer-to-be-falsy': 'warn',
'vitest/prefer-to-be-object': 'warn',
'vitest/prefer-to-have-length': 'warn',
'vitest/consistent-test-filename': 'warn',
'vitest/prefer-describe-function-title': 'warn',
/* relax strict rules in tests */
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
];
+52
View File
@@ -0,0 +1,52 @@
import type { FlatConfigArray } from '../types';
import pluginVue from 'eslint-plugin-vue';
import tseslint from 'typescript-eslint';
import vueParser from 'vue-eslint-parser';
/**
* Vue.js configuration.
*
* Registers `eslint-plugin-vue` with `vue-eslint-parser` (delegating
* `<script lang="ts">` to the TypeScript parser) and enables an opinionated
* subset that enforces Composition API with `<script setup>` and type-based
* declarations. Only the listed rules are turned on — the plugin's large
* `recommended` set is intentionally not pulled in.
*/
export const vue: FlatConfigArray = [
{
name: 'robonen/vue/setup',
files: ['**/*.vue'],
plugins: {
vue: pluginVue,
},
processor: pluginVue.processors['.vue'],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tseslint.parser,
ecmaFeatures: { jsx: true },
extraFileExtensions: ['.vue'],
sourceType: 'module',
},
},
},
{
name: 'robonen/vue/rules',
files: ['**/*.vue'],
rules: {
'vue/no-arrow-functions-in-watch': 'error',
'vue/no-deprecated-destroyed-lifecycle': 'error',
'vue/no-export-in-script-setup': 'error',
'vue/no-lifecycle-after-await': 'error',
'vue/no-multiple-slot-args': 'error',
'vue/no-import-compiler-macros': 'error',
'vue/define-emits-declaration': ['error', 'type-based'],
'vue/define-props-declaration': ['error', 'type-based'],
'vue/prefer-import-from-vue': 'error',
'vue/no-required-prop-with-default': 'warn',
'vue/valid-define-emits': 'error',
'vue/valid-define-props': 'error',
'vue/require-typed-ref': 'warn',
},
},
];
+27
View File
@@ -0,0 +1,27 @@
import type { Linter } from 'eslint';
/**
* A single ESLint flat configuration object.
*
* @see https://eslint.org/docs/latest/use/configure/configuration-files
*/
export type FlatConfig = Linter.Config;
/**
* An array of ESLint flat configuration objects — the shape ESLint
* expects from an `eslint.config.ts` default export.
*/
export type FlatConfigArray = FlatConfig[];
/**
* A flat config rules record (`Partial<Linter.RulesRecord>`).
*/
export type Rules = NonNullable<FlatConfig['rules']>;
/**
* Accepts either a single flat config object or an array of them.
*
* Used by {@link compose} so presets (arrays) and inline overrides
* (single objects) can be passed interchangeably.
*/
export type FlatConfigInput = FlatConfig | FlatConfigArray;
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { compose } from '../src/compose';
import type { FlatConfig } from '../src/types';
describe('compose', () => {
it('should return empty array when no configs provided', () => {
expect(compose()).toEqual([]);
});
it('should wrap a single flat config object into an array', () => {
const config: FlatConfig = {
name: 'a',
rules: { 'no-console': 'warn' },
};
expect(compose(config)).toEqual([config]);
});
it('should flatten preset arrays into a single array', () => {
const preset: FlatConfig[] = [
{ name: 'a', rules: { 'no-console': 'warn' } },
{ name: 'b', rules: { 'no-debugger': 'error' } },
];
expect(compose(preset)).toEqual(preset);
});
it('should preserve order across presets and inline objects', () => {
const presetA: FlatConfig[] = [{ name: 'a' }];
const presetB: FlatConfig[] = [{ name: 'b' }, { name: 'c' }];
const inline: FlatConfig = { name: 'd' };
const result = compose(presetA, presetB, inline);
expect(result.map(c => c.name)).toEqual(['a', 'b', 'c', 'd']);
});
it('should not mutate the input arrays', () => {
const preset: FlatConfig[] = [{ name: 'a' }];
compose(preset, { name: 'b' });
expect(preset).toEqual([{ name: 'a' }]);
});
it('should skip falsy entries for conditional composition', () => {
const result = compose(
{ name: 'a' },
false,
null,
undefined,
{ name: 'b' },
);
expect(result.map(c => c.name)).toEqual(['a', 'b']);
});
it('should compose all presets together preserving order', () => {
const base: FlatConfig[] = [{ name: 'base/setup' }, { name: 'base/rules' }];
const ts: FlatConfig[] = [{ name: 'ts' }];
const custom: FlatConfig = { name: 'custom', rules: { 'no-console': 'off' } };
const result = compose(base, ts, custom);
expect(result.map(c => c.name)).toEqual(['base/setup', 'base/rules', 'ts', 'custom']);
expect(result.at(-1)?.rules).toEqual({ 'no-console': 'off' });
});
});
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.src.json" },
{ "path": "./tsconfig.node.json" }
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@robonen/tsconfig/tsconfig.node.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
},
"include": ["tsdown.config.ts", "vitest.config.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@robonen/tsconfig/tsconfig.base.json",
"compilerOptions": {
"composite": true,
"types": [],
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.src.tsbuildinfo"
},
"include": ["src/**/*.ts", "test/**/*.ts", "eslint.config.ts"]
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'tsdown';
import { sharedConfig } from '@robonen/tsdown';
export default defineConfig({
...sharedConfig,
tsconfig: './tsconfig.src.json',
entry: ['src/index.ts'],
});
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});