diff --git a/apps/docs/src/content/docs/en/eslint-config/advanced.mdx b/apps/docs/src/content/docs/en/eslint-config/advanced.mdx index ae30787..a092601 100644 --- a/apps/docs/src/content/docs/en/eslint-config/advanced.mdx +++ b/apps/docs/src/content/docs/en/eslint-config/advanced.mdx @@ -109,6 +109,20 @@ export default pleaseai({ }) ``` +### Canonical plugin names + +Some linters, such as Bun's `bun lint`, match plugins by their registered name and run renamed plugins in JavaScript instead of natively. Set `canonicalPluginNames: true` to register `@typescript-eslint` and `n` under their original names. The other renames stay: + +```ts title="eslint.config.ts" +import pleaseai from '@pleaseai/eslint-config' + +export default pleaseai({ + canonicalPluginNames: true, +}) +``` + +Rule IDs change to `@typescript-eslint/*` and `n/*`. `ts/*` and `node/*` keys in config `rules` are renamed for you, but `eslint-disable` comments that use them must be updated by hand. + ## View Enabled Rules Use [`@eslint/config-inspector`](https://github.com/eslint/config-inspector) to visualise the fully-resolved config in a browser — every enabled rule, every plugin, every file glob: diff --git a/apps/docs/src/content/docs/ko/eslint-config/advanced.mdx b/apps/docs/src/content/docs/ko/eslint-config/advanced.mdx index 58f2fbd..11bb1db 100644 --- a/apps/docs/src/content/docs/ko/eslint-config/advanced.mdx +++ b/apps/docs/src/content/docs/ko/eslint-config/advanced.mdx @@ -109,6 +109,20 @@ export default pleaseai({ }) ``` +### 원래 플러그인 이름 사용 + +Bun의 `bun lint`처럼 등록된 이름으로 플러그인을 매칭하는 린터는 이름이 바뀐 플러그인을 네이티브가 아닌 JavaScript로 실행합니다. `canonicalPluginNames: true`를 설정하면 `@typescript-eslint`와 `n`이 원래 이름으로 등록됩니다. 나머지 이름 변경은 그대로 유지됩니다: + +```ts title="eslint.config.ts" +import pleaseai from '@pleaseai/eslint-config' + +export default pleaseai({ + canonicalPluginNames: true, +}) +``` + +규칙 ID는 `@typescript-eslint/*`와 `n/*`로 바뀝니다. 설정 `rules`의 `ts/*`, `node/*` 키는 자동으로 변환되지만, 이를 사용하는 `eslint-disable` 주석은 직접 수정해야 합니다. + ## 활성화된 규칙 보기 [`@eslint/config-inspector`](https://github.com/eslint/config-inspector)를 사용하면 완전히 해결된 설정을 브라우저에서 시각화할 수 있습니다 — 활성화된 모든 규칙, 모든 플러그인, 모든 파일 glob까지: diff --git a/packages/eslint-config/README.ko.md b/packages/eslint-config/README.ko.md index aab041a..0fd6d76 100644 --- a/packages/eslint-config/README.ko.md +++ b/packages/eslint-config/README.ko.md @@ -489,6 +489,21 @@ npx @eslint/config-inspector | `style/*` | `@stylistic/*` | [@stylistic/eslint-plugin](https://github.com/eslint-stylistic/eslint-stylistic) | | `test/*` | `vitest/*` | [@vitest/eslint-plugin](https://github.com/vitest-dev/eslint-plugin-vitest) | +### 원래 플러그인 이름 사용 + +Bun의 `bun lint`처럼 등록된 이름으로 플러그인을 매칭하는 린터는 이름이 바뀐 플러그인을 네이티브가 아닌 JavaScript로 실행합니다. `canonicalPluginNames: true`를 설정하면 `@typescript-eslint`와 `n`이 원래 이름으로 등록됩니다. 나머지 이름 변경은 그대로 유지됩니다: + +```ts +// eslint.config.ts +import pleaseai from '@pleaseai/eslint-config' + +export default pleaseai({ + canonicalPluginNames: true, +}) +``` + +규칙 ID는 `@typescript-eslint/*`와 `n/*`로 바뀝니다. 설정 `rules`의 `ts/*`, `node/*` 키는 자동으로 변환되지만, 이를 사용하는 `eslint-disable` 주석은 직접 수정해야 합니다. + ## 버전 정책 `@pleaseai/eslint-config`는 [Semantic Versioning](https://semver.org/)을 따르지만, 여러 가지가 얽힌 스타일 프리셋이기 때문에 **규칙 변경은 breaking change로 취급하지 않습니다**. 아래 표는 `@antfu/eslint-config`에서 상속받은 정책과 동일합니다. diff --git a/packages/eslint-config/README.md b/packages/eslint-config/README.md index 63e67a2..b2019d4 100644 --- a/packages/eslint-config/README.md +++ b/packages/eslint-config/README.md @@ -489,6 +489,21 @@ This config inherits `@antfu/eslint-config`'s plugin renaming for a consistent D | `style/*` | `@stylistic/*` | [@stylistic/eslint-plugin](https://github.com/eslint-stylistic/eslint-stylistic) | | `test/*` | `vitest/*` | [@vitest/eslint-plugin](https://github.com/vitest-dev/eslint-plugin-vitest) | +### Canonical plugin names + +Some linters, such as Bun's `bun lint`, match plugins by their registered name and run renamed plugins in JavaScript instead of natively. Set `canonicalPluginNames: true` to register `@typescript-eslint` and `n` under their original names. The other renames stay: + +```ts +// eslint.config.ts +import pleaseai from '@pleaseai/eslint-config' + +export default pleaseai({ + canonicalPluginNames: true, +}) +``` + +Rule IDs change to `@typescript-eslint/*` and `n/*`. `ts/*` and `node/*` keys in config `rules` are renamed for you, but `eslint-disable` comments that use them must be updated by hand. + ## Versioning Policy `@pleaseai/eslint-config` follows [Semantic Versioning](https://semver.org/), but because it's an opinionated style preset with many moving parts, **rule changes are not treated as breaking changes**. The tables below match the policy inherited from `@antfu/eslint-config`. diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index ed4ebd9..bb46d92 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -25,7 +25,8 @@ "dist" ], "scripts": { - "build": "tsdown" + "build": "tsdown", + "test": "bun test" }, "peerDependencies": { "eslint": "^10.4.0" diff --git a/packages/eslint-config/src/index.ts b/packages/eslint-config/src/index.ts index ff47162..2d2d589 100644 --- a/packages/eslint-config/src/index.ts +++ b/packages/eslint-config/src/index.ts @@ -1,11 +1,32 @@ import type { OptionsConfig, TypedFlatConfigItem } from '@antfu/eslint-config' -import antfu from '@antfu/eslint-config' +import antfu, { defaultPluginRenaming } from '@antfu/eslint-config' + +export interface PleaseaiOptions extends OptionsConfig { + /** + * Register `@typescript-eslint` and `n` under their canonical names instead + * of antfu's `ts` and `node`, so linters that match plugins by name (e.g. + * `bun lint`) can run their rules natively. Rule IDs change accordingly + * (`ts/*` → `@typescript-eslint/*`, `node/*` → `n/*`); `ts/*` and `node/*` + * keys in config `rules` are renamed too, but inline disable comments are not. + * + * @default false + */ + canonicalPluginNames?: boolean +} + +const canonicalPluginRenaming: Record = { + ts: '@typescript-eslint', + node: 'n', +} export function pleaseai( - options: OptionsConfig = {}, + options: PleaseaiOptions = {}, ...userConfigs: TypedFlatConfigItem[] ): ReturnType { - return antfu( + const { canonicalPluginNames = false, ...antfuOptions } = options + const autoRenamePlugins = antfuOptions.autoRenamePlugins ?? true + + const composer = antfu( { lessOpinionated: true, stylistic: { @@ -19,10 +40,26 @@ export function pleaseai( 'antfu/top-level-function': 'error', 'test/prefer-lowercase-title': 'off', }, - ...options, + ...antfuOptions, + // Renames apply in a single pass, so antfu's `@typescript-eslint` → `ts` + // and `n` → `node` entries must be dropped rather than reversed on top. + ...(canonicalPluginNames && { autoRenamePlugins: false }), }, ...userConfigs, ) + + if (!canonicalPluginNames) { + return composer + } + + const renames: Record = autoRenamePlugins + ? Object.fromEntries( + Object.entries(defaultPluginRenaming) + .filter(([, to]) => !(to in canonicalPluginRenaming)), + ) + : {} + + return composer.renamePlugins({ ...renames, ...canonicalPluginRenaming }) } export * from '@antfu/eslint-config' diff --git a/packages/eslint-config/test/index.test.ts b/packages/eslint-config/test/index.test.ts new file mode 100644 index 0000000..b807dbd --- /dev/null +++ b/packages/eslint-config/test/index.test.ts @@ -0,0 +1,50 @@ +import type { PleaseaiOptions, TypedFlatConfigItem } from '../src/index.js' +import { describe, expect, test } from 'bun:test' +import { pleaseai } from '../src/index.js' + +const userConfig: TypedFlatConfigItem = { + files: ['**/*.ts'], + rules: { + 'ts/no-explicit-any': 'error', + '@typescript-eslint/no-unused-vars': 'warn', + 'node/no-sync': 'error', + }, +} + +async function resolve(options: PleaseaiOptions) { + const configs = await pleaseai(options, userConfig) + return { + plugins: new Set(configs.flatMap(c => Object.keys(c.plugins ?? {}))), + rules: Object.assign({}, ...configs.map(c => c.rules ?? {})) as Record, + } +} + +describe('canonicalPluginNames', () => { + test('keeps antfu plugin names by default', async () => { + const { plugins, rules } = await resolve({}) + expect(plugins).toContain('ts') + expect(plugins).toContain('node') + expect(plugins).not.toContain('@typescript-eslint') + expect(plugins).not.toContain('n') + expect(rules['ts/no-unused-vars']).toBe('warn') + }) + + test('registers @typescript-eslint and n under canonical names', async () => { + const { plugins, rules } = await resolve({ canonicalPluginNames: true }) + expect(plugins).toContain('@typescript-eslint') + expect(plugins).toContain('n') + expect(plugins).not.toContain('ts') + expect(plugins).not.toContain('node') + expect(Object.keys(rules).filter(r => r.startsWith('ts/') || r.startsWith('node/'))).toEqual([]) + expect(rules['@typescript-eslint/no-explicit-any']).toBe('error') + expect(rules['@typescript-eslint/no-unused-vars']).toBe('warn') + expect(rules['n/no-sync']).toBe('error') + }) + + test('keeps the other antfu renames', async () => { + const { plugins } = await resolve({ canonicalPluginNames: true }) + expect(plugins).toContain('style') + expect(plugins).toContain('import') + expect(plugins).not.toContain('@stylistic') + }) +})