Prettier&ESLint と Biome を競合させないための Neovim 設定

June 9, 2025

はじめに

TypeScript の開発環境として、長らく Prettier と ESLint の組み合わせが定番でしたが、最近では Biome という選択肢も登場しています。実際の開発では、既存プロジェクトでは PrettierとESLintの組み合わせを、新規プロジェクトでは Biome を使うといったケースも多いのではないでしょうか。

そうなると困るのが Neovim の設定です。両方のツールを有効にしていると、以下のような問題が発生します。

  • 保存時に複数のフォーマッターが動作して競合する
  • 同じルールについて重複した診断メッセージが表示される
  • どちらのツールが適用されているか分かりにくい
  • この記事では、プロジェクトごとに自動的にツールを切り替える Neovim 設定を紹介します。手動での切り替え操作は不要で、プロジェクトルートの設定ファイルを検知して自動で判断する仕組みです。

    前提条件・環境

    以下の環境を前提としています:

  • Neovim 0.10+
  • プラグイン:
  • 開発対象:TypeScript/JavaScript プロジェクト
  • プラグインのインストールと基本的な設定は完了している前提で進めます。

    各設定の詳細解説

    起動するかどうかを動的に判定する

    まずは none-ls 側の設定で Biome の設定ファイルが存在する場合に Prettier を無効化する仕組みを追加します。none-ls のオプションの一つである condition でnone-lsで提供されている root_has_file() を使い、プロジェクトルートに biome.json または biome.jsonc が存在するかチェックしています。これらのファイルが存在する場合、Prettier を無効にします。

    local formatting = require("null-ls").builtins.formatting.prettier formatting.with({ -- ... 他の設定 ... -- Biome の設定ファイルがある場合は Prettier を無効化 condition = function(utils) return not utils.root_has_file({ "biome.json", "biome.jsonc" }) end, -- ... 他の設定 ... })

    次に、ESLint も同様に Biome が存在する場合は無効化します。こちらはLSPの設定に乗せているので、root_dir を動的に設定することで Biome プロジェクトでは ESLint を起動しないようにしています。root_dirnil を返すと、そのバッファでは LSP クライアントがアタッチされません。

    ---@type vim.lsp.Config return { root_dir = function(bufnr, cb) local exclud_dir = vim.fs.root(bufnr, { "biome.json", "biome.jsonc" }) if exclud_dir then cb(nil) return end local root_dir = vim.fs.root(bufnr, { ".eslintrc", ".eslintrc.js", ".eslintrc.json", "eslint.config.js", -- ... }) if root_dir then cb(root_dir) else cb(nil) end end, }

    これだけで目的は果たせるのですが、Biome が提供する自動修正を保存時に自動で実行する設定も加えておきます。

    local function exec_code_action_sync(client, bufnr, action_kind) local params = { textDocument = vim.lsp.util.make_text_document_params(bufnr), range = { start = { line = 0, character = 0 }, ["end"] = { line = vim.fn.line("$"), character = 0 } }, context = { diagnostics = {}, only = { action_kind } }, } local result = vim.lsp.buf_request_sync(bufnr, "textDocument/codeAction", params, 5000) if not result or vim.tbl_isempty(result) then return end for _, res in pairs(result) do if res.result then for _, action in ipairs(res.result) do if action.edit then vim.lsp.util.apply_workspace_edit(action.edit, client.offset_encoding) end end end end end require("lspconfig").biome.setup({ on_attach = function(client, bufnr) -- 保存時に Biome の全機能を実行 vim.api.nvim_create_autocmd("BufWritePre", { buffer = bufnr, callback = function() if client.supports_method("textDocument/codeAction") then -- lintルールの自動修正 exec_code_action_sync(client, bufnr, "source.fixAll") -- インポート文の整理 exec_code_action_sync(client, bufnr, "source.organizeImports") end if client.supports_method("textDocument/formatting") then -- コードフォーマット vim.lsp.buf.format({ bufnr = bufnr, async = false, timeout_ms = 5000, }) end end, }) end, -- ... 他の設定 ... })

    この設定では保存時に以下の処理を順番に実行しています。

  • source.fixAll:リンティングエラーの自動修正
  • source.organizeImports:インポート文の並び替えと未使用インポートの削除
  • textDocument/formatting:コードフォーマット
  • 重要なのは async = false で同期実行している点です。これにより、他のフォーマッターが同時に動作することを防ぎます。

    これはおまけですが、プロジェクトで使用しているパッケージマネージャーに応じて Biome の起動コマンドを切り替えることもしています。pnpm を使っているプロジェクトでは pnpm で起動し、それ以外では npx を使います。

    -- pnpm を使っているかチェック local pnpm_root = vim.fs.root(0, { "pnpm-lock.yaml", "pnpm-workspace.yaml" }) require("lspconfig").biome.setup({ -- pnpm プロジェクトなら pnpm 経由で、そうでなければ npx で実行 cmd = pnpm_root and { "pnpm", "biome", "lsp-proxy" } or { "npx", "biome", "lsp-proxy" }, -- ... 他の設定 ... })

    さいごに

    Prettier/ESLint と Biome の両方を使い分ける必要がある場合は、こうした条件分岐を設定に組み込むことでスムーズなプロジェクト間の移動が可能になります。設定ファイルの検知による自動切り替えは、他のツール(例えば Deno と Node.js の切り替えなど)にも応用できる手法なので、覚えておくと便利です。