Migrate from ESLint to bun lint

bun lint reads your eslint.config.js and prints the same report as ESLint 10. In most projects you only change the command.

package.json
{
  "scripts": {
    "lint": "eslint --max-warnings 0", 
    "lint": "bun lint --max-warnings 0" 
  }
}

Flags and arguments stay the same: eslint --fix src becomes bun lint --fix src. Change CI, lint-staged, and Git hooks too.

Edit the script first. If package.json has a lint script, bun lint runs that script, so you'd still get ESLint.


Run it. You get the same rules, messages, and exit code as with eslint.

terminal
bun run lint
$ bun lint --max-warnings 0

/app/src/index.ts
  1:1   warning  Open an issue instead of leaving a TODO              local/no-todo
  3:7   error    'fallback' is never reassigned. Use 'const' instead  prefer-const
  4:27  error    Invalid typeof comparison value                      valid-typeof

✖ 3 problems (2 errors, 1 warning)
  1 error and 0 warnings potentially fixable with the `--fix` option.

Linted 3 files [108.90ms]
error: script "lint" exited with code 1

Run --doctor to see what Bun does with your config. It doesn't lint or write anything.

terminal
bun lint --doctor
Using eslint.config.js (ESLint flat config)

    3 files to lint

   25 rules run natively
    1 rule runs in JavaScript
        lint/plugin.js    1

Still needed:   eslint (typescript-eslint needs it)
                typescript-eslint (eslint.config.js loads it)
  • Natively: the rule is built into Bun. That covers ESLint's core rules, typescript-eslint, react, react-hooks, import, 15 rules of n, and prettier/prettier.
  • In JavaScript: Bun loads the plugin and runs it like ESLint does. It works, but it's slower. See ESLint plugins.

The last lines tell you which packages you still need. Bun runs your eslint.config.js, so everything that file imports has to stay. Here that's typescript-eslint, which needs eslint. Packages you no longer need are listed under You can remove.


You can drop lint-staged. --staged lints only the staged files, and with --fix it stages the fixed files again.

.husky/pre-commit
npx lint-staged 
bun lint --staged --fix 

Type-aware rules need no changes. If your config asks for types, they run.

eslint.config.js
import tseslint from "typescript-eslint";

export default tseslint.config(...tseslint.configs.recommendedTypeChecked, {
  languageOptions: { parserOptions: { projectService: true } },
});

The types come from Bun's built-in type checker (bun check), not from the typescript package. It behaves like TypeScript 7.


Still on ESLint 8? .eslintrc.*, eslintConfig in package.json, and .eslintignore work when there is no eslint.config.js. So do ESLint 8's flags.

terminal
eslint --ext .js,.ts,.tsx src 
bun lint --ext .js,.ts,.tsx src 

If your config asks for something Bun can't do, Bun lints everything else, tells you what it skipped, and exits with code 2.

error: 1 rule did not run because rules that run in JavaScript cannot use type information: local/typed
  Everything else was linted. Use --allow-unsupported to make this a warning.

There are two such cases: a JavaScript rule that needs types, and files in other languages when the eslint package isn't installed. Pass --allow-unsupported to turn the error into a warning.


If the project also has an oxlint config, Bun uses that one and tells you.

note: Using .oxlintrc.json. eslint.config.js is ignored. Use --config to pick it.

Pick the ESLint config with --config.

package.json
{
  "scripts": {
    "lint": "oxlint && eslint", 
    "lint": "bun lint && bun lint --config eslint.config.js" 
  }
}

Differences from ESLint#

If bun lint and ESLint 10 disagree about a rule, that's a bug. Open an issue. These differences are intentional:

  • Built-in rules have a fixed version, whatever you have installed: ESLint 10.12, typescript-eslint 8.71, eslint-plugin-react 7.37, react-hooks 7.1, import 2.32, n 18.4. --no-native-plugin-rules runs your installed plugins instead.
  • Deprecated formatting rules don't run: semi, quotes, indent, and so on. Use bun fmt or @stylistic/eslint-plugin.
  • No custom formatters. -f only takes built-in formats such as stylish, json, unix, junit, and sarif.
  • No --inspect-config or --mcp.
  • --cache does nothing. Every run lints every file.
  • No config file is not an error. Bun uses its defaults.
  • Syntax errors can be worded differently. Bun's parser replaces espree, @typescript-eslint/parser, and @babel/eslint-parser.
  • A type-aware rule without type information is skipped, with a warning. typescript-eslint throws.
  • A file that no tsconfig.json includes is still linted, with the nearest tsconfig.json. ESLint stops.
  • --fix never writes a file that no longer parses. Bun drops those fixes and warns.
  • console.log in your config file is hidden unless the config fails to load.
  • One extra line on stderr with the number of files and the time.
  • In GitHub Actions, ::error and ::warning lines follow the stylish report on stdout, so problems show up in the pull request diff. If a problem matcher such as the one from actions/setup-node already does that, you get each annotation twice. -f json and the other machine-readable formats don't have these lines.

See also: