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:
@@ -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.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { base, compose, imports, stylistic, typescript } from './src';
|
||||||
|
|
||||||
|
export default compose(base, typescript, imports, stylistic);
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/oxlint",
|
"name": "@robonen/eslint",
|
||||||
"version": "0.0.2",
|
"version": "0.0.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Composable oxlint configuration presets",
|
"description": "Composable ESLint flat configuration presets",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"oxlint",
|
"eslint",
|
||||||
"oxc",
|
"eslint-config",
|
||||||
|
"flat-config",
|
||||||
"linter",
|
"linter",
|
||||||
"config",
|
"config",
|
||||||
"presets"
|
"presets"
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "configs/oxlint"
|
"directory": "configs/eslint"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.30.3",
|
"packageManager": "pnpm@10.30.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -37,28 +38,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint:check": "oxlint -c oxlint.config.ts",
|
"lint:check": "eslint .",
|
||||||
"lint:fix": "oxlint -c oxlint.config.ts --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"build": "tsdown"
|
"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": {
|
"devDependencies": {
|
||||||
"@robonen/oxlint": "workspace:*",
|
"@robonen/eslint": "workspace:*",
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"@robonen/tsdown": "workspace:*",
|
"@robonen/tsdown": "workspace:*",
|
||||||
"@stylistic/eslint-plugin": "catalog:",
|
"eslint": "catalog:",
|
||||||
"oxlint": "catalog:",
|
|
||||||
"tsdown": "catalog:"
|
"tsdown": "catalog:"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"oxlint": ">=1.0.0",
|
"eslint": ">=9.0.0"
|
||||||
"@stylistic/eslint-plugin": ">=4.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@stylistic/eslint-plugin": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Rules Reference
|
# Rules Reference
|
||||||
|
|
||||||
Документация по preset-ам `@robonen/oxlint`: что включает каждый preset и какие правила чаще всего влияют на код.
|
Документация по preset-ам `@robonen/eslint`: что включает каждый preset и какие правила чаще всего влияют на код.
|
||||||
|
|
||||||
## Presets
|
## Presets
|
||||||
|
|
||||||
@@ -18,4 +18,4 @@
|
|||||||
- `Key Rules` — ключевые правила из preset-а (не полный dump).
|
- `Key Rules` — ключевые правила из preset-а (не полный dump).
|
||||||
- `Examples` — минимальные `good/bad` примеры.
|
- `Examples` — минимальные `good/bad` примеры.
|
||||||
|
|
||||||
Для точного источника правил см. файлы в `configs/oxlint/src/presets/*.ts`.
|
Для точного источника правил см. файлы в `configs/eslint/src/presets/*.ts`.
|
||||||
@@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Базовый quality-профиль для JS/TS-проектов: корректность, анти-паттерны, безопасные дефолты.
|
Базовый quality-профиль для JS/TS-проектов: корректность, анти-паттерны, безопасные дефолты. Включает `@eslint/js` recommended (аналог oxlint-категории `correctness`) + правила `unicorn`.
|
||||||
|
|
||||||
## Key Rules
|
## Key Rules
|
||||||
|
|
||||||
- `eslint/eqeqeq`: запрещает `==`, требует `===`.
|
- `eqeqeq`: запрещает `==`, требует `===`.
|
||||||
- `eslint/no-unused-vars`: запрещает неиспользуемые переменные (кроме `_name`).
|
- `no-unused-vars`: запрещает неиспользуемые переменные (кроме `_name`).
|
||||||
- `eslint/no-eval`, `eslint/no-var`, `eslint/prefer-const`.
|
- `no-eval`, `no-var`, `prefer-const`.
|
||||||
- `unicorn/prefer-node-protocol`: требует `node:` для built-in модулей.
|
- `unicorn/prefer-node-protocol`: требует `node:` для built-in модулей.
|
||||||
- `unicorn/no-thenable`: запрещает thenable-объекты.
|
- `unicorn/no-thenable`: запрещает thenable-объекты.
|
||||||
- `oxc/*` correctness правила (`bad-comparison-sequence`, `missing-throw` и др.).
|
|
||||||
|
> Правила `oxc/*` из `@robonen/oxlint` не имеют аналога в ESLint и были убраны;
|
||||||
|
> их назначение покрывается `@eslint/js` recommended и `unicorn`.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Чистые границы модулей и предсказуемые импорты.
|
Чистые границы модулей и предсказуемые импорты (через `eslint-plugin-import-x`).
|
||||||
|
|
||||||
## Key Rules
|
## Key Rules
|
||||||
|
|
||||||
- `import/no-duplicates`.
|
- `import-x/no-duplicates`.
|
||||||
- `import/no-self-import`.
|
- `import-x/no-self-import`.
|
||||||
- `import/no-cycle` (warn).
|
- `import-x/no-cycle` (warn).
|
||||||
- `import/no-mutable-exports`.
|
- `import-x/no-mutable-exports`.
|
||||||
- `import/consistent-type-specifier-style`: `prefer-top-level`.
|
- `import-x/consistent-type-specifier-style`: `prefer-top-level`.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -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');
|
||||||
|
```
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
- `vitest/no-import-node-test`.
|
- `vitest/no-import-node-test`.
|
||||||
- `vitest/prefer-to-be-truthy`, `vitest/prefer-to-be-falsy`.
|
- `vitest/prefer-to-be-truthy`, `vitest/prefer-to-be-falsy`.
|
||||||
- `vitest/prefer-to-have-length`.
|
- `vitest/prefer-to-have-length`.
|
||||||
- Relaxations: `eslint/no-unused-vars` и `typescript/no-explicit-any` выключены для тестов.
|
- Relaxations: `no-unused-vars` и `@typescript-eslint/no-explicit-any` выключены для тестов.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export { base } from './base';
|
export { base, ignores } from './base';
|
||||||
export { typescript } from './typescript';
|
export { typescript } from './typescript';
|
||||||
export { vue } from './vue';
|
export { vue } from './vue';
|
||||||
export { vitest } from './vitest';
|
export { vitest } from './vitest';
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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;
|
||||||
@@ -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' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.src.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@ import { sharedConfig } from '@robonen/tsdown';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
|
tsconfig: './tsconfig.src.json',
|
||||||
entry: ['src/index.ts'],
|
entry: ['src/index.ts'],
|
||||||
});
|
});
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# @robonen/oxlint
|
|
||||||
|
|
||||||
Composable [oxlint](https://oxc.rs/docs/guide/usage/linter.html) configuration presets.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install -D @robonen/oxlint oxlint
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Create `oxlint.config.ts` in your project root:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { defineConfig } from 'oxlint';
|
|
||||||
import { compose, base, typescript, vue, vitest, imports } from '@robonen/oxlint';
|
|
||||||
|
|
||||||
export default defineConfig(
|
|
||||||
compose(base, typescript, vue, vitest, imports),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Append custom rules after presets to override them:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
compose(base, typescript, {
|
|
||||||
rules: { 'eslint/no-console': 'off' },
|
|
||||||
ignorePatterns: ['dist'],
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Presets
|
|
||||||
|
|
||||||
| Preset | Description |
|
|
||||||
| ------------ | -------------------------------------------------- |
|
|
||||||
| `base` | Core eslint, oxc, unicorn rules |
|
|
||||||
| `typescript` | TypeScript-specific rules (via overrides) |
|
|
||||||
| `vue` | Vue 3 Composition API / `<script setup>` rules |
|
|
||||||
| `vitest` | Test file rules (via overrides) |
|
|
||||||
| `imports` | Import rules (cycles, duplicates, ordering) |
|
|
||||||
| `node` | Node.js-specific rules |
|
|
||||||
|
|
||||||
## Rules Documentation
|
|
||||||
|
|
||||||
Подробные описания правил и `good/bad` примеры вынесены в отдельную директорию:
|
|
||||||
|
|
||||||
- `rules/README.md`
|
|
||||||
- `rules/base.md`
|
|
||||||
- `rules/typescript.md`
|
|
||||||
- `rules/vue.md`
|
|
||||||
- `rules/vitest.md`
|
|
||||||
- `rules/imports.md`
|
|
||||||
- `rules/node.md`
|
|
||||||
- `rules/stylistic.md`
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### `compose(...configs: OxlintConfig[]): OxlintConfig`
|
|
||||||
|
|
||||||
Merges multiple configs into one:
|
|
||||||
|
|
||||||
- **plugins** — union (deduplicated)
|
|
||||||
- **rules / categories** — last wins
|
|
||||||
- **overrides / ignorePatterns** — concatenated
|
|
||||||
- **env / globals** — shallow merge
|
|
||||||
- **settings** — deep merge
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { defineConfig } from 'oxlint';
|
|
||||||
import { compose, base, typescript, imports, stylistic } from '@robonen/oxlint';
|
|
||||||
|
|
||||||
export default defineConfig(compose(base, typescript, imports, stylistic));
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# node preset
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Node.js-правила и окружение `env.node = true`.
|
|
||||||
|
|
||||||
## Key Rules
|
|
||||||
|
|
||||||
- `node/no-exports-assign`: запрещает перезапись `exports`.
|
|
||||||
- `node/no-new-require`: запрещает `new require(...)`.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// ✅ Good
|
|
||||||
module.exports = { run };
|
|
||||||
const mod = require('./mod');
|
|
||||||
|
|
||||||
// ❌ Bad
|
|
||||||
exports = { run };
|
|
||||||
const bad = new require('./mod');
|
|
||||||
```
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# typescript preset
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
TypeScript-правила для `.ts/.tsx/.mts/.cts` через `overrides`.
|
|
||||||
|
|
||||||
## Key Rules
|
|
||||||
|
|
||||||
- `typescript/consistent-type-imports`: выносит типы в `import type`.
|
|
||||||
- `typescript/no-import-type-side-effects`: запрещает сайд-эффекты в type import.
|
|
||||||
- `typescript/prefer-as-const`.
|
|
||||||
- `typescript/no-namespace`, `typescript/triple-slash-reference`.
|
|
||||||
- `typescript/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;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import type { OxlintConfig } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deep merge two objects. Arrays are concatenated, objects are recursively merged.
|
|
||||||
*/
|
|
||||||
function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
|
|
||||||
const result = { ...target };
|
|
||||||
|
|
||||||
for (const key of Object.keys(source)) {
|
|
||||||
const targetValue = target[key];
|
|
||||||
const sourceValue = source[key];
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)
|
|
||||||
&& typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)
|
|
||||||
) {
|
|
||||||
result[key] = deepMerge(
|
|
||||||
targetValue as Record<string, unknown>,
|
|
||||||
sourceValue as Record<string, unknown>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
result[key] = sourceValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compose multiple oxlint configurations into a single config.
|
|
||||||
*
|
|
||||||
* - `plugins` — union (deduplicated)
|
|
||||||
* - `jsPlugins` — union (deduplicated by specifier)
|
|
||||||
* - `categories` — later configs override earlier
|
|
||||||
* - `rules` — later configs override earlier
|
|
||||||
* - `overrides` — concatenated
|
|
||||||
* - `env` — merged (later overrides earlier)
|
|
||||||
* - `globals` — merged (later overrides earlier)
|
|
||||||
* - `settings` — deep-merged
|
|
||||||
* - `ignorePatterns` — concatenated
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* import { compose, base, typescript, vue } from '@robonen/oxlint';
|
|
||||||
* import { defineConfig } from 'oxlint';
|
|
||||||
*
|
|
||||||
* export default defineConfig(
|
|
||||||
* compose(base, typescript, vue, {
|
|
||||||
* rules: { 'eslint/no-console': 'off' },
|
|
||||||
* }),
|
|
||||||
* );
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function compose(...configs: OxlintConfig[]): OxlintConfig {
|
|
||||||
const result: OxlintConfig = {};
|
|
||||||
|
|
||||||
for (const config of configs) {
|
|
||||||
// Plugins — union with dedup
|
|
||||||
if (config.plugins?.length) {
|
|
||||||
result.plugins = Array.from(new Set([...(result.plugins ?? []), ...config.plugins]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// JS Plugins — union with dedup by specifier
|
|
||||||
if (config.jsPlugins?.length) {
|
|
||||||
const existing = result.jsPlugins ?? [];
|
|
||||||
const seen = new Set(existing.map(e => typeof e === 'string' ? e : e.specifier));
|
|
||||||
|
|
||||||
for (const entry of config.jsPlugins) {
|
|
||||||
const specifier = typeof entry === 'string' ? entry : entry.specifier;
|
|
||||||
if (!seen.has(specifier)) {
|
|
||||||
seen.add(specifier);
|
|
||||||
existing.push(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.jsPlugins = existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categories — shallow merge
|
|
||||||
if (config.categories) {
|
|
||||||
result.categories = { ...result.categories, ...config.categories };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rules — shallow merge (later overrides earlier)
|
|
||||||
if (config.rules) {
|
|
||||||
result.rules = { ...result.rules, ...config.rules };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overrides — concatenate
|
|
||||||
if (config.overrides?.length) {
|
|
||||||
result.overrides = [...(result.overrides ?? []), ...config.overrides];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Env — shallow merge
|
|
||||||
if (config.env) {
|
|
||||||
result.env = { ...result.env, ...config.env };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Globals — shallow merge
|
|
||||||
if (config.globals) {
|
|
||||||
result.globals = { ...result.globals, ...config.globals };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings — deep merge
|
|
||||||
if (config.settings) {
|
|
||||||
result.settings = deepMerge(
|
|
||||||
(result.settings ?? {}) as Record<string, unknown>,
|
|
||||||
config.settings as Record<string, unknown>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore patterns — concatenate
|
|
||||||
if (config.ignorePatterns?.length) {
|
|
||||||
result.ignorePatterns = [...(result.ignorePatterns ?? []), ...config.ignorePatterns];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/* Compose */
|
|
||||||
export { compose } from './compose';
|
|
||||||
|
|
||||||
/* Presets */
|
|
||||||
export { base, typescript, vue, vitest, imports, node, stylistic } from './presets';
|
|
||||||
|
|
||||||
/* Types */
|
|
||||||
export type {
|
|
||||||
OxlintConfig,
|
|
||||||
OxlintOverride,
|
|
||||||
OxlintEnv,
|
|
||||||
OxlintGlobals,
|
|
||||||
ExternalPluginEntry,
|
|
||||||
AllowWarnDeny,
|
|
||||||
DummyRule,
|
|
||||||
DummyRuleMap,
|
|
||||||
RuleCategories,
|
|
||||||
} from './types';
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import type { OxlintConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base configuration for any JavaScript/TypeScript project.
|
|
||||||
*
|
|
||||||
* Enables `correctness` category and opinionated rules from
|
|
||||||
* `eslint`, `oxc`, and `unicorn` plugins.
|
|
||||||
*/
|
|
||||||
export const base: OxlintConfig = {
|
|
||||||
plugins: ['eslint', 'oxc', 'unicorn'],
|
|
||||||
|
|
||||||
categories: {
|
|
||||||
correctness: 'error',
|
|
||||||
},
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
/* ── eslint core ──────────────────────────────────────── */
|
|
||||||
'eslint/eqeqeq': 'error',
|
|
||||||
'eslint/no-console': 'warn',
|
|
||||||
'eslint/no-debugger': 'error',
|
|
||||||
'eslint/no-eval': 'error',
|
|
||||||
'eslint/no-var': 'error',
|
|
||||||
'eslint/prefer-const': 'error',
|
|
||||||
'eslint/prefer-template': 'warn',
|
|
||||||
'eslint/no-useless-constructor': 'warn',
|
|
||||||
'eslint/no-useless-rename': 'warn',
|
|
||||||
'eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
|
||||||
'eslint/no-self-compare': 'error',
|
|
||||||
'eslint/no-template-curly-in-string': 'warn',
|
|
||||||
'eslint/no-throw-literal': 'error',
|
|
||||||
'eslint/no-return-assign': 'warn',
|
|
||||||
'eslint/no-else-return': 'warn',
|
|
||||||
'eslint/no-lonely-if': 'warn',
|
|
||||||
'eslint/no-unneeded-ternary': 'warn',
|
|
||||||
'eslint/prefer-object-spread': 'warn',
|
|
||||||
'eslint/prefer-exponentiation-operator': 'warn',
|
|
||||||
'eslint/no-useless-computed-key': 'warn',
|
|
||||||
'eslint/no-useless-concat': 'warn',
|
|
||||||
'eslint/curly': 'off',
|
|
||||||
|
|
||||||
/* ── unicorn ──────────────────────────────────────────── */
|
|
||||||
'unicorn/prefer-node-protocol': 'error',
|
|
||||||
'unicorn/no-instanceof-array': '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',
|
|
||||||
|
|
||||||
/* ── oxc ──────────────────────────────────────────────── */
|
|
||||||
'oxc/no-accumulating-spread': 'warn',
|
|
||||||
'oxc/bad-comparison-sequence': 'error',
|
|
||||||
'oxc/bad-min-max-func': 'error',
|
|
||||||
'oxc/bad-object-literal-comparison': 'error',
|
|
||||||
'oxc/const-comparisons': 'error',
|
|
||||||
'oxc/double-comparisons': 'error',
|
|
||||||
'oxc/erasing-op': 'error',
|
|
||||||
'oxc/missing-throw': 'error',
|
|
||||||
'oxc/bad-bitwise-operator': 'error',
|
|
||||||
'oxc/bad-char-at-comparison': 'error',
|
|
||||||
'oxc/bad-replace-all-arg': 'error',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import type { OxlintConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import plugin rules for clean module boundaries.
|
|
||||||
*/
|
|
||||||
export const imports: OxlintConfig = {
|
|
||||||
plugins: ['import'],
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
'import/no-duplicates': 'error',
|
|
||||||
'import/no-self-import': 'error',
|
|
||||||
'import/no-cycle': 'warn',
|
|
||||||
'import/first': 'warn',
|
|
||||||
'import/no-mutable-exports': 'error',
|
|
||||||
'import/no-amd': 'error',
|
|
||||||
'import/no-commonjs': 'warn',
|
|
||||||
'import/no-empty-named-blocks': 'warn',
|
|
||||||
'import/consistent-type-specifier-style': ['warn', 'prefer-top-level'],
|
|
||||||
|
|
||||||
'sort-imports': 'warn',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { OxlintConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Node.js-specific rules.
|
|
||||||
*/
|
|
||||||
export const node: OxlintConfig = {
|
|
||||||
plugins: ['node'],
|
|
||||||
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
'node/no-exports-assign': 'error',
|
|
||||||
'node/no-new-require': 'error',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import type { OxlintConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stylistic formatting rules via `@stylistic/eslint-plugin`.
|
|
||||||
*
|
|
||||||
* Uses the plugin's `customize()` defaults:
|
|
||||||
* - indent: 2
|
|
||||||
* - quotes: single
|
|
||||||
* - semi: true
|
|
||||||
* - braceStyle: stroustrup
|
|
||||||
* - commaDangle: always-multiline
|
|
||||||
* - arrowParens: false (as-needed)
|
|
||||||
* - blockSpacing: true
|
|
||||||
* - quoteProps: consistent-as-needed
|
|
||||||
* - jsx: true
|
|
||||||
*
|
|
||||||
* Requires `@stylistic/eslint-plugin` to be installed.
|
|
||||||
*
|
|
||||||
* @see https://eslint.style/guide/config-presets
|
|
||||||
*/
|
|
||||||
export const stylistic: OxlintConfig = {
|
|
||||||
jsPlugins: ['@stylistic/eslint-plugin'],
|
|
||||||
|
|
||||||
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',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { OxlintConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TypeScript-specific rules.
|
|
||||||
*
|
|
||||||
* Applied via `overrides` for `*.ts`, `*.tsx`, `*.mts`, `*.cts` files.
|
|
||||||
*/
|
|
||||||
export const typescript: OxlintConfig = {
|
|
||||||
plugins: ['typescript'],
|
|
||||||
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
|
||||||
rules: {
|
|
||||||
'typescript/consistent-type-imports': 'error',
|
|
||||||
'typescript/no-explicit-any': 'off',
|
|
||||||
'typescript/no-non-null-assertion': 'off',
|
|
||||||
'typescript/prefer-as-const': 'error',
|
|
||||||
'typescript/no-empty-object-type': 'warn',
|
|
||||||
'typescript/no-wrapper-object-types': 'error',
|
|
||||||
'typescript/no-duplicate-enum-values': 'error',
|
|
||||||
'typescript/no-unsafe-declaration-merging': 'error',
|
|
||||||
'typescript/no-import-type-side-effects': 'error',
|
|
||||||
'typescript/no-useless-empty-export': 'warn',
|
|
||||||
'typescript/no-inferrable-types': 'warn',
|
|
||||||
'typescript/prefer-function-type': 'warn',
|
|
||||||
'typescript/ban-tslint-comment': 'error',
|
|
||||||
'typescript/consistent-type-definitions': ['warn', 'interface'],
|
|
||||||
'typescript/prefer-for-of': 'warn',
|
|
||||||
'typescript/no-unnecessary-type-constraint': 'warn',
|
|
||||||
'typescript/adjacent-overload-signatures': 'warn',
|
|
||||||
'typescript/array-type': ['warn', { default: 'array-simple' }],
|
|
||||||
'typescript/no-this-alias': 'error',
|
|
||||||
'typescript/triple-slash-reference': 'error',
|
|
||||||
'typescript/no-namespace': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import type { OxlintConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vitest rules for test files.
|
|
||||||
*
|
|
||||||
* Applied via `overrides` for common test file patterns.
|
|
||||||
*/
|
|
||||||
export const vitest: OxlintConfig = {
|
|
||||||
plugins: ['vitest'],
|
|
||||||
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'**/*.test.{ts,tsx,js,jsx}',
|
|
||||||
'**/*.spec.{ts,tsx,js,jsx}',
|
|
||||||
'**/test/**/*.{ts,tsx,js,jsx}',
|
|
||||||
'**/__tests__/**/*.{ts,tsx,js,jsx}',
|
|
||||||
],
|
|
||||||
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 */
|
|
||||||
'eslint/no-unused-vars': 'off',
|
|
||||||
'typescript/no-explicit-any': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { OxlintConfig } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vue.js-specific rules.
|
|
||||||
*
|
|
||||||
* Enforces Composition API with `<script setup>` and type-based declarations.
|
|
||||||
*/
|
|
||||||
export const vue: OxlintConfig = {
|
|
||||||
plugins: ['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',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* Re-exported configuration types from `oxlint`.
|
|
||||||
*
|
|
||||||
* Keeps the preset API in sync with the oxlint CLI without
|
|
||||||
* maintaining a separate copy of the types.
|
|
||||||
*
|
|
||||||
* @see https://oxc.rs/docs/guide/usage/linter/config-file-reference.html
|
|
||||||
*/
|
|
||||||
export type {
|
|
||||||
OxlintConfig,
|
|
||||||
OxlintOverride,
|
|
||||||
OxlintEnv,
|
|
||||||
OxlintGlobals,
|
|
||||||
ExternalPluginEntry,
|
|
||||||
AllowWarnDeny,
|
|
||||||
DummyRule,
|
|
||||||
DummyRuleMap,
|
|
||||||
RuleCategories,
|
|
||||||
} from 'oxlint';
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { compose } from '../src/compose';
|
|
||||||
import type { OxlintConfig } from '../src/types';
|
|
||||||
|
|
||||||
describe('compose', () => {
|
|
||||||
it('should return empty config when no configs provided', () => {
|
|
||||||
expect(compose()).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the same config when one config provided', () => {
|
|
||||||
const config: OxlintConfig = {
|
|
||||||
plugins: ['eslint'],
|
|
||||||
rules: { 'eslint/no-console': 'warn' },
|
|
||||||
};
|
|
||||||
const result = compose(config);
|
|
||||||
expect(result.plugins).toEqual(['eslint']);
|
|
||||||
expect(result.rules).toEqual({ 'eslint/no-console': 'warn' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should merge plugins with dedup', () => {
|
|
||||||
const a: OxlintConfig = { plugins: ['eslint', 'oxc'] };
|
|
||||||
const b: OxlintConfig = { plugins: ['oxc', 'typescript'] };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.plugins).toEqual(['eslint', 'oxc', 'typescript']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should override rules from later configs', () => {
|
|
||||||
const a: OxlintConfig = { rules: { 'eslint/no-console': 'error', 'eslint/eqeqeq': 'warn' } };
|
|
||||||
const b: OxlintConfig = { rules: { 'eslint/no-console': 'off' } };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.rules).toEqual({
|
|
||||||
'eslint/no-console': 'off',
|
|
||||||
'eslint/eqeqeq': 'warn',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should override categories from later configs', () => {
|
|
||||||
const a: OxlintConfig = { categories: { correctness: 'error', suspicious: 'warn' } };
|
|
||||||
const b: OxlintConfig = { categories: { suspicious: 'off' } };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.categories).toEqual({
|
|
||||||
correctness: 'error',
|
|
||||||
suspicious: 'off',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should concatenate overrides', () => {
|
|
||||||
const a: OxlintConfig = {
|
|
||||||
overrides: [{ files: ['**/*.ts'], rules: { 'typescript/no-explicit-any': 'warn' } }],
|
|
||||||
};
|
|
||||||
const b: OxlintConfig = {
|
|
||||||
overrides: [{ files: ['**/*.test.ts'], rules: { 'eslint/no-unused-vars': 'off' } }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.overrides).toHaveLength(2);
|
|
||||||
expect(result.overrides?.[0]?.files).toEqual(['**/*.ts']);
|
|
||||||
expect(result.overrides?.[1]?.files).toEqual(['**/*.test.ts']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should merge env', () => {
|
|
||||||
const a: OxlintConfig = { env: { browser: true } };
|
|
||||||
const b: OxlintConfig = { env: { node: true } };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.env).toEqual({ browser: true, node: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should merge globals', () => {
|
|
||||||
const a: OxlintConfig = { globals: { MY_VAR: 'readonly' } };
|
|
||||||
const b: OxlintConfig = { globals: { ANOTHER: 'writable' } };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.globals).toEqual({ MY_VAR: 'readonly', ANOTHER: 'writable' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deep merge settings', () => {
|
|
||||||
const a: OxlintConfig = {
|
|
||||||
settings: {
|
|
||||||
react: { version: '18.2.0' },
|
|
||||||
next: { rootDir: 'apps/' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const b: OxlintConfig = {
|
|
||||||
settings: {
|
|
||||||
react: { linkComponents: [{ name: 'Link', linkAttribute: 'to', attributes: ['to'] }] },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.settings).toEqual({
|
|
||||||
react: {
|
|
||||||
version: '18.2.0',
|
|
||||||
linkComponents: [{ name: 'Link', linkAttribute: 'to', attributes: ['to'] }],
|
|
||||||
},
|
|
||||||
next: { rootDir: 'apps/' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should concatenate ignorePatterns', () => {
|
|
||||||
const a: OxlintConfig = { ignorePatterns: ['dist'] };
|
|
||||||
const b: OxlintConfig = { ignorePatterns: ['node_modules', 'coverage'] };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.ignorePatterns).toEqual(['dist', 'node_modules', 'coverage']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle composing all presets together', () => {
|
|
||||||
const base: OxlintConfig = {
|
|
||||||
plugins: ['eslint', 'oxc'],
|
|
||||||
categories: { correctness: 'error' },
|
|
||||||
rules: { 'eslint/no-console': 'warn' },
|
|
||||||
};
|
|
||||||
const ts: OxlintConfig = {
|
|
||||||
plugins: ['typescript'],
|
|
||||||
overrides: [{ files: ['**/*.ts'], rules: { 'typescript/no-explicit-any': 'warn' } }],
|
|
||||||
};
|
|
||||||
const custom: OxlintConfig = {
|
|
||||||
rules: { 'eslint/no-console': 'off' },
|
|
||||||
ignorePatterns: ['dist'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = compose(base, ts, custom);
|
|
||||||
|
|
||||||
expect(result.plugins).toEqual(['eslint', 'oxc', 'typescript']);
|
|
||||||
expect(result.categories).toEqual({ correctness: 'error' });
|
|
||||||
expect(result.rules).toEqual({ 'eslint/no-console': 'off' });
|
|
||||||
expect(result.overrides).toHaveLength(1);
|
|
||||||
expect(result.ignorePatterns).toEqual(['dist']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip undefined/empty fields', () => {
|
|
||||||
const a: OxlintConfig = { plugins: ['eslint'] };
|
|
||||||
const b: OxlintConfig = { rules: { 'eslint/no-console': 'warn' } };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.plugins).toEqual(['eslint']);
|
|
||||||
expect(result.rules).toEqual({ 'eslint/no-console': 'warn' });
|
|
||||||
expect(result.overrides).toBeUndefined();
|
|
||||||
expect(result.env).toBeUndefined();
|
|
||||||
expect(result.settings).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should concatenate jsPlugins with dedup by specifier', () => {
|
|
||||||
const a: OxlintConfig = { jsPlugins: ['eslint-plugin-foo'] };
|
|
||||||
const b: OxlintConfig = { jsPlugins: ['eslint-plugin-foo', 'eslint-plugin-bar'] };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.jsPlugins).toEqual(['eslint-plugin-foo', 'eslint-plugin-bar']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dedup jsPlugins with mixed string and object entries', () => {
|
|
||||||
const a: OxlintConfig = { jsPlugins: ['eslint-plugin-foo'] };
|
|
||||||
const b: OxlintConfig = { jsPlugins: [{ name: 'foo', specifier: 'eslint-plugin-foo' }] };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.jsPlugins).toEqual(['eslint-plugin-foo']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should keep jsPlugins and plugins independent', () => {
|
|
||||||
const a: OxlintConfig = { plugins: ['eslint'], jsPlugins: ['eslint-plugin-foo'] };
|
|
||||||
const b: OxlintConfig = { plugins: ['typescript'], jsPlugins: ['eslint-plugin-bar'] };
|
|
||||||
|
|
||||||
const result = compose(a, b);
|
|
||||||
expect(result.plugins).toEqual(['eslint', 'typescript']);
|
|
||||||
expect(result.jsPlugins).toEqual(['eslint-plugin-foo', 'eslint-plugin-bar']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@robonen/tsconfig/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "src"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+85
-10
@@ -1,6 +1,6 @@
|
|||||||
# @robonen/tsconfig
|
# @robonen/tsconfig
|
||||||
|
|
||||||
Shared base TypeScript configuration.
|
Shared TypeScript configurations.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -8,20 +8,95 @@ Shared base TypeScript configuration.
|
|||||||
pnpm install -D @robonen/tsconfig
|
pnpm install -D @robonen/tsconfig
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Presets
|
||||||
|
|
||||||
|
| Preset | Extends | Use for |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `tsconfig.base.json` | — | Node / isomorphic libraries (`lib: ESNext`, no DOM) |
|
||||||
|
| `tsconfig.dom.json` | base | Browser libraries (adds `DOM`, `DOM.Iterable`) |
|
||||||
|
| `tsconfig.vue.json` | dom | Vue SFC libraries / apps (adds `jsx`, `vueCompilerOptions`) |
|
||||||
|
| `tsconfig.node.json` | base | Build/test tooling files (`*.config.ts`) — adds `types: ["node"]`, no DOM |
|
||||||
|
| `tsconfig.json` | base | Default alias for `base` (bare `@robonen/tsconfig` import) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Extend from it in your `tsconfig.json`:
|
Pick the preset that matches the package and extend it:
|
||||||
|
|
||||||
```json
|
```jsonc
|
||||||
|
// Node / isomorphic library
|
||||||
|
{ "extends": "@robonen/tsconfig/tsconfig.base.json" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// Browser library
|
||||||
|
{ "extends": "@robonen/tsconfig/tsconfig.dom.json" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// Vue package, with path aliases
|
||||||
{
|
{
|
||||||
"extends": "@robonen/tsconfig/tsconfig.json"
|
"extends": "@robonen/tsconfig/tsconfig.vue.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's Included
|
> Path aliases resolve relative to the `tsconfig.json` location — `baseUrl` is
|
||||||
|
> intentionally omitted (deprecated, removed in TypeScript 7.0).
|
||||||
|
|
||||||
- **Target / Module**: ESNext with Bundler resolution
|
## Project references (DOM + Node split)
|
||||||
- **Strict mode**: `strict`, `noUncheckedIndexedAccess`
|
|
||||||
- **Module safety**: `verbatimModuleSyntax`, `isolatedModules`
|
Most packages contain two environments: browser/library `src` (DOM) and Node
|
||||||
- **Declarations**: `declaration` enabled
|
tooling files (`vite.config.ts`, `vitest.config.ts`, `tsdown.config.ts`). They
|
||||||
- **Interop**: `esModuleInterop`, `allowJs`, `resolveJsonModule`
|
are split into separate projects wired with references, so `src` never sees Node
|
||||||
|
globals and config files never see `DOM`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// tsconfig.json — solution root, tools (tsdown/vitest/editor) target src below
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.src.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// tsconfig.src.json — the library code
|
||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"types": [],
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.src.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// tsconfig.node.json — build/test tooling files
|
||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.node.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["*.config.ts"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Type-check the whole package (both projects) with `tsc -b` / `vue-tsc -b`.
|
||||||
|
Point `tsdown` at the src project (`tsconfig: './tsconfig.src.json'`) since the
|
||||||
|
root has no `compilerOptions`.
|
||||||
|
|
||||||
|
## What's included (base)
|
||||||
|
|
||||||
|
- **Target / Module**: `ESNext` with `module: Preserve` + `Bundler` resolution
|
||||||
|
- **Strict mode**: `strict`, `noUncheckedIndexedAccess`, `noImplicitOverride`,
|
||||||
|
`noImplicitReturns`, `noFallthroughCasesInSwitch`, `noUncheckedSideEffectImports`
|
||||||
|
- **Module safety**: `verbatimModuleSyntax`, `isolatedModules`, `moduleDetection: force`
|
||||||
|
- **Type-check only**: `noEmit` (declarations/output are produced by `tsdown`)
|
||||||
|
- **Interop**: `esModuleInterop`, `resolveJsonModule`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/tsconfig",
|
"name": "@robonen/tsconfig",
|
||||||
"version": "0.0.2",
|
"version": "0.1.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Base typescript configuration for projects",
|
"description": "Base typescript configuration for projects",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -20,7 +20,11 @@
|
|||||||
"node": ">=24.13.1"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"**tsconfig.json"
|
"tsconfig.json",
|
||||||
|
"tsconfig.base.json",
|
||||||
|
"tsconfig.dom.json",
|
||||||
|
"tsconfig.node.json",
|
||||||
|
"tsconfig.vue.json"
|
||||||
],
|
],
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "@robonen base (type-check, no DOM)",
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Modules */
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
||||||
|
/* Language and environment */
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
|
||||||
|
/* Type checking (strict) */
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
/* Emit is delegated to the bundler (tsdown); tsc is type-check only */
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", ".output", "coverage"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "@robonen dom (browser libraries)",
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"display": "Base TypeScript Configuration",
|
"display": "@robonen base (default)",
|
||||||
"compilerOptions": {
|
"extends": "./tsconfig.base.json"
|
||||||
/* Basic Options */
|
}
|
||||||
"module": "ESNext",
|
|
||||||
"noEmit": true,
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"target": "ESNext",
|
|
||||||
"outDir": "dist",
|
|
||||||
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"isolatedModules": true,
|
|
||||||
"removeComments": false,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
|
||||||
"strict": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
|
|
||||||
/* Library transpiling */
|
|
||||||
"declaration": true,
|
|
||||||
// "composite": true,
|
|
||||||
"sourceMap": false,
|
|
||||||
"declarationMap": false
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist", ".output", "coverage"]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "@robonen node (build/test tooling files)",
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "@robonen vue (Vue SFC libraries / apps)",
|
||||||
|
"extends": "./tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve"
|
||||||
|
},
|
||||||
|
"vueCompilerOptions": {
|
||||||
|
"strictTemplates": true,
|
||||||
|
"fallthroughAttributes": true,
|
||||||
|
"inferTemplateDollarAttrs": true,
|
||||||
|
"inferTemplateDollarEl": true,
|
||||||
|
"inferTemplateDollarRefs": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user