ESLint plugins
Run ESLint plugins in bun lint, and write your own rules
bun lint runs ESLint plugins, both from npm and your own. Rules get the same context, sourceCode, scopes, and fixers as in ESLint.
This plugin has one rule, which reports TODO comments:
const noTodo = {
meta: {
type: "suggestion",
messages: { todo: "Open an issue instead of leaving a TODO." },
schema: [],
},
create(context) {
const { sourceCode } = context;
return {
Program() {
for (const comment of sourceCode.getAllComments()) {
if (comment.value.includes("TODO")) {
context.report({ loc: comment.loc, messageId: "todo" });
}
}
},
};
},
};
export default {
meta: { name: "local" },
rules: { "no-todo": noTodo },
};Load it in bunfig.toml. Bun's default rules stay on, and the plugin's rules are added to them.
[lint]
plugins = ["./lint/plugin.js"]
[lint.rules]
"local/no-todo" = "warn"bun lint1 | // TODO: handle errors
^
warn: Open an issue instead of leaving a TODO. local/no-todo
at src/index.js:1:1
1 problem (0 errors, 1 warning)
Linted 3 files [15.40ms]You don't need the eslint package. To learn the API, read ESLint's guide to plugins.
If you have an ESLint config or an oxlint config, load it there as usual:
import local from "./lint/plugin.js";
export default [
{
plugins: { local },
rules: { "local/no-todo": "warn" },
},
];{
"jsPlugins": ["./lint/plugin.js"],
"rules": { "local/no-todo": "warn" }
}plugins in bunfig.toml works next to those files too, and next to an .eslintrc.*. A plugin can only be loaded once under a name, so load it in one place.
Plugins from npm#
Install the package and name it. ESLint's short names work, so "perfectionist" is eslint-plugin-perfectionist.
bun add -d eslint-plugin-perfectionist[lint]
plugins = ["perfectionist"]
[lint.rules]
"perfectionist/sort-imports" = "error"A path is relative to bunfig.toml. A rule's prefix is the plugin's own name, meta.name, without eslint-plugin-. To use another prefix:
[lint]
plugins = [{ name = "sort", specifier = "eslint-plugin-perfectionist" }]
[lint.rules]
"sort/sort-imports" = "error"You don't load the plugins that are built in, such as react, import, or unicorn. Their rules work without it. Next to an ESLint or oxlint config, naming one turns it on, as plugins in that file does.
A config file can't use the processors or languages of a plugin that is loaded this way. For those, import the plugin in the config file.
For one run, use --plugin. Next to an ESLint config its prefix is the name you give the flag, as in ESLint:
bun lint --plugin perfectionist --rule 'perfectionist/sort-imports: error'With an ESLint config, eight plugins run natively: @typescript-eslint, react, react-hooks, import, n, prettier, json, and markdown. Everything else runs from node_modules, including unicorn, jsx-a11y, promise, jest, and vitest. Bun's built-in versions of those follow oxlint, so Bun only uses them with bunfig.toml or an oxlint config.
bun lint --doctor shows which of your rules run natively and which run in JavaScript.
Performance#
JavaScript rules are slower than native ones. bun lint --timing shows how much time they take.
- Only turn on a plugin's rules for the files that need them. Files without JavaScript rules pay nothing.
- Put a plugin in its own module. If you define it inside
eslint.config.js, every thread has to run the whole config file.
Run the installed package instead#
To run a built-in plugin from node_modules, for example because you pin an older version:
bun lint --no-native-plugin-rules
bun lint --native-plugin-rules=@typescript-eslint,import # only these two stay native[lint]
nativePluginRules = ["@typescript-eslint", "import"]ESLint's core rules are always native. Keep @typescript-eslint native if you use type-aware rules, because JavaScript rules don't get types.
nativePluginRules picks which version of a plugin runs. It doesn't turn rules on or off. To do that, see lint.rules.
prettier/prettier#
prettier/prettier from eslint-plugin-prettier is native. It compares each file with the output of bun fmt and never starts Prettier.
Bun falls back to the installed package, and says why, when it can't guarantee the same output as your Prettier:
- You have a version other than Prettier 3.9.
- You use a Prettier plugin that
bun fmtdoesn't have. - The file isn't JavaScript or TypeScript.
Without Prettier, use bun/format. It checks files against bun fmt.
Other languages#
In an ESLint config, a file can have a language from a plugin. Bun supports the languages of two plugins natively:
@eslint/json:json/json,json/jsonc, andjson/json5, with its rules.@eslint/markdown:markdown/commonmarkandmarkdown/gfm, with its rules.
Bun parses these files with its own parsers and runs its own versions of the rules, so eslint doesn't have to be installed.
import json from "@eslint/json";
export default [
{
files: ["**/*.json"],
plugins: { json },
language: "json/json",
rules: { "json/no-duplicate-keys": "error" },
},
];Comments such as // eslint-disable-next-line work in JSONC and JSON5, as they do in ESLint. A file with a syntax error gets a single error and no rules run. ESLint does the same, but the wording of the error is Bun's.
import markdown from "@eslint/markdown";
export default [
{
files: ["**/*.md"],
plugins: { markdown },
language: "markdown/gfm",
languageOptions: { frontmatter: "yaml" },
rules: { "markdown/no-missing-label-refs": "error" },
},
];markdown.configs.recommended works too, and so do languageOptions.frontmatter, languageOptions.math, and comments such as <!-- eslint-disable-next-line markdown/no-html -->.
The plugin parses Markdown with micromark. Bun uses the parser behind Bun.markdown. The two agree on almost every document. In the few places where micromark deviates from the CommonMark and GFM specs, Bun follows the specs. For example, <a href=b/c> is an HTML tag to Bun and plain text to the plugin.
Bun hands every other file to the eslint package in your project, so keep eslint 8.57 or later installed if you use:
- A language from another plugin, such as
css/css. - A JSON or Markdown rule that isn't built in, such as one of your own. The whole file then goes to
eslint. - A parser for another file type, such as
vue-eslint-parser,svelte-eslint-parser, oryaml-eslint-parser. eslint-plugin-html.
You still run bun lint, and these files show up in the same report. bun lint --doctor shows how many files go to eslint.
Processors#
Processors work. This config lints the code blocks in Markdown files:
import markdown from "@eslint/markdown";
export default [
{ files: ["**/*.md"], plugins: { markdown }, processor: "markdown/markdown" },
{ files: ["**/*.md/*.js"], rules: { "no-var": "error" } },
];Notes#
- JavaScript rules don't get types.
parserServices.programisnull, even with--type-aware. A rule that calls typescript-eslint'sgetParserServices()doesn't run, andbun lintexits with code2. - Plugins run on several threads. Each thread imports the plugin, so module-level state is per thread, not per run.
- A renamed built-in plugin runs in JavaScript. For example,
@antfu/eslint-configregisters@typescript-eslintasts. - A rule that throws ends the run with exit code
2. With an oxlint config, Bun reports the error for that file and continues. - A report at
loc: { line: 0, column: 0 }is at1:1./* eslint-disable */at the start of the file disables it, except with an ESLint or oxlint config, where neither tool lets a comment disable it. With an ESLint config, thestylish,json, andunixformats print0:1, like ESLint. With an oxlint config, alocoutside the file that oxlint rejects makes the rule throw, as it does in oxlint. - Type-aware rules skip files that have a processor.