From 972cfeafafcef7ffacce4e97e53bcb53de1aa3ec Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 16 Oct 2026 03:16:31 +0900 Subject: [PATCH] feat(eslint-config): add canonicalPluginNames option for native bun lint rules Add an opt-in canonicalPluginNames option to pleaseai(). It registers @typescript-eslint and n under their canonical names instead of antfu's ts and node aliases, so rule names resolve natively under bun lint. The option turns off antfu's autoRenamePlugins and applies antfu's defaultPluginRenaming minus those two entries, plus ts to @typescript-eslint and node to n. Also add bun tests with a test script, and document the option in the READMEs and the docs site (en/ko). Closes #56 --- .../docs/en/eslint-config/advanced.mdx | 14 ++++++ .../docs/ko/eslint-config/advanced.mdx | 14 ++++++ packages/eslint-config/README.ko.md | 15 ++++++ packages/eslint-config/README.md | 15 ++++++ packages/eslint-config/package.json | 3 +- packages/eslint-config/src/index.ts | 45 +++++++++++++++-- packages/eslint-config/test/index.test.ts | 50 +++++++++++++++++++ 7 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 packages/eslint-config/test/index.test.ts 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') + }) +})