Repository navigation
feat(eslint-config): restore canonical plugin names so Bun's lint runs typescript-eslint and n natively #56
Description
Context
Bun's upcoming bun lint (tested on 1.4.4-canary.1) is an ESLint-compatible linter that reads eslint.config.* directly. With an ESLint config, it runs these rules natively: ESLint core, @typescript-eslint, import, react, react-hooks, 15 rules of n, and prettier/prettier. Every other plugin is loaded and run in JavaScript, which is slower.
The plugins docs call out our base config directly: plugins registered under a different name run in JavaScript, e.g. "@antfu/eslint-config registers @typescript-eslint as ts". @antfu/eslint-config 9.5.1 (our current base) still applies defaultPluginRenaming (@typescript-eslint → ts, n → node, import-lite → import, @stylistic → style, …).
Measurements
Repo: a ~2,250-file TypeScript/Markdown monorepo using @pleaseai/eslint-config 0.0.4 (antfu 8.3.0) plus eslint-plugin-sonarjs and a few local rules. ESLint 9.33, Intel i7-9700K 8 cores.
Result parity: bun lint reported the same errors and warnings as ESLint across all 2,250 files (19,407 errors / 264 warnings). The only difference was the reported column of 4 complexity findings.
Native vs. JS rules (bun lint --doctor --config eslint.config.mjs):
| Config | Rule prefixes | Native / JS |
|---|---|---|
| as-is | ts/…, node/… |
106 / 231 |
pleaseai({ autoRenamePlugins: false, … }) |
still ts/…, node/… |
106 / 231 |
pleaseai({…}).renamePlugins({ ts: '@typescript-eslint', node: 'n' }) |
@typescript-eslint/…, n/… |
133 / 204 |
autoRenamePlugins: false has no effect because antfu's internal config blocks register the plugins as ts/node from the start. Reversing the rename with the composer moves all 27 typescript-eslint rules to the native path. Findings stay identical apart from the rule-ID prefix.
Timing, JS/TS files only (652 files, md/json/yml/toml excluded; 2 alternating runs). The machine was under heavy load (load average 150+), so treat these as indicative only:
| Run | ESLint 9.33 | bun lint as-is |
bun lint renamed |
|---|---|---|---|
| 1 | 50.2 s | 19.8 s | 11.8 s |
| 2 | 31.7 s | 20.0 s | 17.1 s |
Proposal
- Add an opt-in option (name TBD, e.g.
canonicalPluginNames: true) that applies.renamePlugins({ ts: '@typescript-eslint', node: 'n' })to the composed config. - Evaluate replacing
eslint-plugin-import-litewitheslint-plugin-import. Bun matches native rules by package, so import-lite's 6 rules stay in JS even under theimportname. This trades away antfu's lighter dependency. - Later: consider moving
@stylisticformatting (65 rules, all in JS) tobun fmt, after checking that its Markdown output matches Prettier's.
Caveats
- Renaming changes rule IDs. Consumers'
eslint-disable … ts/…comments andrules: { 'ts/…': … }overrides break, so this should start as opt-in. Making it the default is a breaking change. regexp(60),jsdoc(17), andunicorn(15) still run in JS. Bun ships unicorn natively, but only exposes it on the oxlint config path.- Markdown/JSON/YAML/TOML files are always handed to the installed ESLint, so whole-repo gains are limited for docs-heavy repos.
bun lintis not in a stable release yet (the stable 1.4.3 has nolintcommand), and apackage.jsonlintscript shadows the built-in. These numbers should be re-measured on the release.
Activity
amondnet commentedon Oct 15, 2026
Follow-up: Bun's built-in unicorn rules are not reachable from an ESLint config
Bun ships 138
unicorn/*rules natively (rules list), but the docs don't say how they get enabled. I tested each path on1.4.4-canary.1:.oxlintrc.jsononlybunfig.toml[lint.rules]onlyno-nullreported)--native-plugin-rules=unicorn@typescript-eslint,react-hooks,import,n,react,prettier--rule 'unicorn/no-null: error'eslint-plugin-unicorn(JS unicorn rules 15 → 16)bunfig.toml[lint.rules]Could not find plugin "unicorn" in configurationThe built-in unicorn rules only work with an oxlint config or a bunfig-only setup. Once an ESLint config is present,
bunfig.tomlrules are read with ESLint semantics: an unregistered plugin is an error, and a registered one runs in JS. For this package, that means the 15 unicorn rules antfu enables stay in JS no matter how the plugins are named. Renaming plugins (this issue's proposal) doesn't change that.There are two ways to change it. One is moving the shared config to an oxlint config, which is a large migration. The other is asking Bun to match
eslint-plugin-unicornnatively on the ESLint config path, the way it already does for@typescript-eslintandn. The second is much cheaper and would help every antfu-based consumer. I'd treat it as out of scope here and track it upstream.amondnet commentedon Oct 15, 2026
Correction and follow-up: unicorn is excluded by design, and
oxlint.config.tscan share configs as a packageCorrection. My previous comment said the docs don't explain how the built-in unicorn rules get enabled. They do: the plugins page states it.
So keeping unicorn in JS on the ESLint config path is intentional, not a gap. Asking Bun for an
eslint-plugin-unicorn-compatible native path would mean asking it to change a documented design, so I no longer think that's the cheap option.Why it's excluded. I ran the 15 unicorn rules antfu enables through
eslint-plugin-unicorn64.0.0 and through Bun's built-ins, on a fixture with violations, non-violations, and option edge cases. The two disagree:eslint-plugin-unicorn64.0.0prefer-includesarr.some(x => x === 1)(also1 === x, block body)prefer-type-errorif (!(v instanceof Date)) { throw new Error('bad') }prefer-node-protocolimport cp = require('child_process')escape-case"uppercase"number-literal-case{ hexadecimalValue: "uppercase" }The other 10 rules reported the same lines:
error-message,new-for-builtins,no-instanceof-builtins,no-new-array,no-new-buffer,consistent-empty-array-spread,prefer-dom-node-text-content,prefer-number-properties,prefer-string-starts-ends-with, andthrow-new-error. On the real codebase (650 JS/TS files), both sides found the same single finding.Sharing a native config as a package.
bunfig.tomlhas noextends, so I checked how a config built from Bun's built-in rules could be published and reused. The test used a fake package@acme/lint-configinnode_modules, on1.4.4-canary.1:bunfig.toml[lint] extends = "..."Unknown key "extends" in [lint]bunfig.toml[lint] config = "node_modules/@acme/lint-config/oxlintrc.json"[lint.rules]overrides applybunfig.toml[lint] config = "@acme/lint-config/oxlint"(package specifier).oxlintrc.json"extends": ["./node_modules/@acme/lint-config/oxlintrc.json"]rulesoverrides apply.oxlintrc.json"extends": ["@acme/lint-config/oxlint"](package specifier)oxlint.config.tswithimport base from '@acme/lint-config', spread and overriddeneslint.config.mjsimporting the same package (unicorn plugin not registered)1 rule in eslint.config.mjs is not supported: unicorn/no-nulloxlint.config.tscomposes like a flat config: import a package, spread it, override. On that path every built-in rule runs natively, unicorn included. The JSON-based options can only point at anode_modules/...file path.What this means for this repo. A Bun-native shared config is possible as a separate package (e.g.
@pleaseai/oxlint-config) consumed fromoxlint.config.ts. That is a move from ESLint to oxlint, though. Plugins Bun doesn't ship (@stylistic, perfectionist, regexp, jsdoc) would run in JS viajsPlugins, and unicorn would follow oxlint's port, with the differences above. Not yet tested:jsPluginsandoverridesinsideoxlint.config.ts, and importing a package that ships TS source rather than.mjs.The proposal in this issue (restoring canonical plugin names) still stands for the ESLint path. The oxlint-config package is a separate, larger option.
amondnet commentedon Oct 16, 2026
Follow-up: migrating this config to an oxlint config with
jsPluginsI converted a consuming repo's
eslint.config.mjs(@pleaseai/eslint-config, defaults) with@oxlint/migrate1.86.0 and ran it withbun linton1.4.4-canary.1. For JS/TS, the result nearly matches ESLint once four things are fixed. Non-JS files don't migrate.Raw migrate output
@oxlint/migratemigrated 387 rules and skipped 20:no-undef,no-unreachable-loopno-restricted-syntax, fournode/*rules, sevenjsdoc/*rules,vitest/no-only-testsdot-notationand othersIt also dropped
sort-imports, which it defers to Oxfmt. It warned that theyaml-eslint-parser,toml-eslint-parser, andparser-plainparsers and ignore lists inside overrides are not supported.The output also didn't load as-is. Two
jsPluginspackage names were wrong (eslint-comments,e18e), and theimport/consistent-type-specifier-styleoption"top-level"had to become"prefer-top-level". After those fixes,--doctorreported 115 native rules, 151 JS rules, and 10jsdoc/*rules that don't run (bun lint has no "jsdoc" rules).Four fixes, compared against ESLint
Same 650 JS/TS files; ESLint with the original config reports 341 problems.
complexity,max-lines*, and sonarjs report nothing; rules that should be off in scripts and tests are on**/*.?([cm])ts) don't match inbun lint, so those overrides are silently skipped. Brace globs (**/*.{,[cm]}ts) work. Reported upstream as oven-sh/bun#45470eslint-plugin-nandeslint-plugin-jsdocasjsPluginsnodeandjsdocare reserved built-in names (Plugin name 'node' is reserved), so they need aliases (n-js,jsdoc-js). Migrate had also droppednode/prefer-global/process: offfrom the test override; without it, 231 extra problems appear@stylisticasstyle@stylistic/..., so existingeslint-disable style/...comments stop working (86 extra).{ "name": "style", "specifier": "@stylistic/eslint-plugin" }fixes itAfter step 3,
--doctorreports 145 native rules, 178 JS rules, and 0 that don't run. The JS rules are@stylistic65, regexp 60, jsdoc 17, e18e 9, n 8, antfu 7, eslint-comments 4, perfectionist 4, unused-imports 2, command 1, and sonarjs 1.The remaining differences:
vitest/prefer-hooks-in-order(+35): The rules judge hook order the same way, but they check different hooks.@vitest/eslint-plugin1.6.19 checks hooks imported fromvitestor used as globals, and skips hooks imported frombun:test. Bun's built-in rule, an oxlint port, checks hooks from any import source. All 35 problems come from 19 test files that import their hooks frombun:test. In a minimal repro (beforeAll,afterAll,beforeEach,afterEach), both report 2 problems for avitestimport and for globals, while for abun:testimport ESLint reports 0 and Bun reports 2. oxlint 1.86.0 matches Bun.max-lines(18): same count, different reported line.no-use-before-define(1): same finding under a different rule ID (ts/vs. core).Every other rule matched by count and line.
Limits
.ts, 21.js, 8.mjs). The JSON, YAML, TOML, and Markdown overrides carry over asjsPlugins, but no parser reads those files. JSON key sorting, YAML and TOML rules, andeslint-plugin-formatMarkdown formatting all go away.typescript/,node/,vitest/), so a consumer'seslint-disable ts/...ortest/...comments won't match.oxlint.config.ts. I tested the JSON output, not the same config written asoxlint.config.ts.Speed
I timed
bun lintand ESLint on the same 650 files, alternating, two runs each, at load average 20–40:bun lint(oxlint config): 12.8–18.6 sThat is about 2–3× faster. It is similar to the ESLint-config path with canonical plugin names, because 178 rules still run in JS.
Takeaway
A shareable
oxlint.config.ts(for example,@pleaseai/oxlint-config) could carry the JS/TS rules with near-ESLint results if it includes:n/jsdocJS pluginsstylealias for@stylisticvitest/prefer-hooks-in-orderfor repos whose tests import frombun:testIt can't replace this config on its own. JSON, YAML, TOML, and Markdown would still need ESLint or
bun fmt. For the issue as filed, restoring canonical names in this config remains the smaller change.amondnet commentedon Oct 16, 2026
Summary: ESLint-config path vs. oxlint-config path in
bun lintAll numbers come from
bun lint1.4.4-canary.1 run on the same consuming repo as the comments above.eslint.config.mjs).oxlintrc.json/oxlint.config.ts)complexitycolumns differ)stylealias, and theoffoverrides that migrate droppednode_modules, so results match ESLinteslint-plugin-unicorn64.0.0: 3 rules differ in results, 2 in option handling.vitest/*also checks hooks imported frombun:testts/...,test/...)typescript/,vitest/)Recommendation
bun lint. It is a drop-in replacement with ESLint-identical results, including non-JS files..renamePlugins({ ts: '@typescript-eslint', node: 'n' })), as an option or a new default. This raises the native rule count from 106 to 133. It is a breaking change: consumers must update their disable comments and rule overrides.oxlint.config.tspackage, with ESLint orbun fmtstill covering non-JS files.Caveat:
bun lintis still a canary build. These differences should be re-checked on a stable release before we act on them.amondnet commentedon Oct 17, 2026
Re-measured on this repo: antfu 9.5.1, canary
1.4.4-canary.1+5f5799ed1This run used this repo's own
eslint.config.ts(@pleaseai/eslint-configon@antfu/eslint-config9.5.1,astro: true), ESLint 10.12.0, and 158 files. The canary was built from a 2026-10-15 commit, so it includes neither oven-sh/bun#45482 (the #45470 extglob fix) nor oven-sh/bun#45595 (native JSON/Markdown).ts/,node/).renamePlugins({ ts: '@typescript-eslint', node: 'n' })--doctor)// eslint-disable-next-line ts/…Definition for rule 'ts/no-explicit-any' was not foundThere's no speedup at this size. Fixed costs dominate: loading the JS plugins takes about 1.2 s, and 112 files are handed to the installed ESLint.
New findings
node→ngains nothing. Bun runs only part ofeslint-plugin-nnatively:n/no-process-exitruns natively, whilen/no-sync,n/no-path-concat, andn/no-exports-assignstay in JS, even when registered asn. None of the 8node/*rules antfu enables runs natively. The +27 in the original measurement also came entirely from typescript-eslint. For antfu's defaults, only thets→@typescript-eslinthalf of this proposal pays off.package.jsonlintscript shadows the built-in, hereeslint . --cache. Only--doctorreaches the built-in linter. The documentedbun lint --no-scriptflag isn't in this canary yet.--doctorprintsYou can remove: @pleaseai/eslint-configeven thougheslint.config.tsimports it. Every consumer of this package would get the same wrong advice. This looks worth reporting upstream.testplugin inside the config, and--timingreportsloaded the whole config file for the plugin "test", so every thread evaluates the whole config.Tracking
I'm keeping a tracking doc (
docs/research/bun-lint-compatibility.md) with a watch list and re-check procedure untilbun lintships in a stable Bun. These numbers should be re-run on that release before we act on the proposal.docs: track antfu and eslint-config-xo under bun lint
docs: record bun lint results with native JSON and Markdown (oven-sh/…
amondnet commentedon Oct 17, 2026
Upstream comparison (antfu, eslint-config-xo) and the oven-sh/bun#45595 PR build
These follow up the previous measurement. The tracking doc is in #60 (
docs/research/bun-lint-compatibility.md).Upstream configs under canary
1.4.4-canary.1+5f5799ed1Both configs were measured in scratch projects with ESLint 10.13.0, TypeScript 6.0.3, and typescript-eslint 8.71.2. The fixtures were the same violation files plus 21 TS files from
packages/cli/src.@antfu/eslint-config9.5.1eslint-config-xo6.0.0ts,node,import,style, …)@typescript-eslint,n,import-x,@stylistic, …)--doctor).renamePlugins({ ts: '@typescript-eslint' })nrules nativeimportrules nativeimport-lite)import-x;--native-plugin-rules=importchanges nothing)projectService: true)xo5.0.1 CLIunicorn/consistent-boolean-nameand 6regexp/*. These rules read types throughparserServices, but Bun gives types only to its native rules. The JS rules report less, and--doctorstill says 0 rules aren't run. For unicorn, the missed pattern confirms the cause:const ok = await confirmOverwrite(...)is a boolean only the type checker can see. For regexp, the cause is inferred. Our config isn't type-aware today, but it would hit this once antfu's type-aware mode (typescript: { tsconfigPath }) is enabled.nandimport. eslint-config-xo shows that with canonical names, typescript-eslint runs natively, but none of thenorimport-xrules do. That strengthens the earlier point: thets→@typescript-eslinthalf of this proposal pays off, and thenode→nhalf doesn't. Replacingimport-litewitheslint-plugin-import(proposal item 2) is still untested.Upstream positions:
ts/rename on purpose (Docs: switching from 'ts/' prefix to '@typescript-eslint/' antfu/eslint-config#527), so canonical names have to come from this package.bun lintwith an ESLint config already is. Bun hasn't been discussed there yet.eslint-config-xo"so should be easy to just use any ESLint compatible runner". Thexo5.0.1 CLI still depends oneslint-config-xo^3.0.2.oven-sh/bun#45595 (native JSON and Markdown), PR build
1.4.4-canary.1+a1ab4b531No canary includes it yet, so I used the PR's CI build (
bunx bun-pr 45595). It also doesn't contain the oven-sh/bun#45482 fix.@antfu/eslint-config9.5.1eslint-config-xo6.0.0package.json)@eslint/markdownrules antfu enables now run natively. Code blocks inside Markdown give the same results as before.eslint-plugin-jsonconjsonc-eslint-parser, not@eslint/json, so all 17 JSON files here still go to ESLint, along with YAML and TOML. eslint-config-xo uses@eslint/json, so its JSON runs natively, exceptpackage.json.--no-scriptworks in this build, sobun lintcan reach the built-in linter even with apackage.jsonlintscript.Everything here should be re-run on a release canary and then on the stable release before we act on the proposal.
feat(eslint-config): add opt-in canonicalPluginNames for bun lint nat…
docs(ci): say why extra eslint workers lose