Skip to main content

How to Migrate from ESLint to Biome

·PkgPulse Team

TL;DR

Biome replaces ESLint + Prettier in a single tool and runs 25x faster. Biome covers ~200+ ESLint rules natively — enough for most projects. Migration is 30-60 minutes: install Biome, run biome migrate eslint, review the output, delete .eslintrc. The main limitation: Biome doesn't support custom plugins or rules from the ecosystem (eslint-plugin-react-hooks still needed). Most teams run Biome for the 95% and keep a minimal ESLint config for the remaining plugins.

Key Takeaways

  • 25x faster than ESLint — Rust-based, processes files in parallel
  • Replaces both ESLint + Prettier — one tool for linting and formatting
  • ~200+ rules built-in — covers recommended configs for most plugins
  • No plugin system yet — third-party plugins not supported
  • biome migrate eslint — official command that auto-converts your config

Step 1: Install Biome

npm install -D --save-exact @biomejs/biome

# Initialize config
npx @biomejs/biome init

# This creates biome.json at project root

Step 2: Run the Migration Tool

# Auto-convert your ESLint config to Biome
npx @biomejs/biome migrate eslint --write

# This command:
# 1. Reads your .eslintrc / eslint.config.js
# 2. Maps rules to Biome equivalents
# 3. Updates biome.json with matched rules
# 4. Reports rules it couldn't migrate

# Example output:
# ✔ Migrated 45 rules
# ℹ 3 rules have no Biome equivalent:
#   - react-hooks/rules-of-hooks (use eslint-plugin-react-hooks)
#   - import/no-cycle (no equivalent)
#   - custom-rules/my-rule (custom plugin)

Step 3: Review biome.json

// biome.json — after migration
{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "a11y": {
        "useAltText": "error",
        "noAutofocus": "warn"
      },
      "complexity": {
        "noExtraBooleanCast": "error",
        "noMultipleSpacesInRegularExpressionLiteral": "error"
      },
      "correctness": {
        "noUnusedVariables": "error",
        "useExhaustiveDependencies": "warn"  // replaces react-hooks/exhaustive-deps
      },
      "security": {
        "noDangerouslySetInnerHtml": "warn"
      },
      "style": {
        "noVar": "error",
        "useConst": "error",
        "useSingleVarDeclarator": "error"
      },
      "suspicious": {
        "noConsoleLog": "warn",
        "noDebugger": "error",
        "noDoubleEquals": "error"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",          // Was: "singleQuote": true in Prettier
      "trailingCommas": "es5",         // Was: "trailingComma": "es5" in Prettier
      "semicolons": "always"           // Was: "semi": true in Prettier
    }
  },
  "files": {
    "ignore": [
      "dist/**",
      "build/**",
      "node_modules/**",
      "*.min.js"
    ]
  }
}

Step 4: Update package.json Scripts

// Before (ESLint + Prettier):
{
  "scripts": {
    "lint": "eslint src/ --ext .ts,.tsx --report-unused-disable-directives",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write src/",
    "format:check": "prettier --check src/"
  }
}

// After (Biome only):
{
  "scripts": {
    "lint": "biome lint --apply .",
    "format": "biome format --write .",
    "check": "biome check --apply .",  // Lint + format in one command
    "ci": "biome ci ."                 // CI: no --apply (fail on issues)
  }
}

Step 5: Handle Unmigrated Rules

# Rules with no Biome equivalent need to stay in ESLint
# Common examples:
# - eslint-plugin-react-hooks (rules-of-hooks, exhaustive-deps)
#   → react-hooks/rules-of-hooks: partial Biome equivalent (correctness/useHookAtTopLevel)
#   → react-hooks/exhaustive-deps: correctness/useExhaustiveDependencies ✅ covered
# - eslint-plugin-import (no-cycle, order)
#   → import ordering: Biome's organizeImports handles basic sorting
#   → circular deps: no equivalent
# - Custom company rules → no equivalent
// Minimal .eslintrc for rules Biome can't handle
// Keep this tiny — Biome handles the rest
{
  "plugins": ["react-hooks", "import"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "import/no-cycle": "warn"
  },
  "ignorePatterns": [
    // Ignore files already covered by Biome
    // This prevents double-linting
  ]
}

Step 6: CI Configuration

# .github/workflows/lint.yml
name: Lint & Format

on: [push, pull_request]

jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Biome
        uses: biomejs/setup-biome@v2
        with:
          version: latest

      - name: Run Biome CI
        run: biome ci .
        # biome ci: no --apply flag, fails on any issue
        # Reports: linting errors + formatting differences

ESLint Rule → Biome Rule Mapping

ESLint RuleBiome Equivalent
no-unused-varscorrectness/noUnusedVariables
no-varstyle/noVar
prefer-conststyle/useConst
no-consolesuspicious/noConsoleLog
no-debuggersuspicious/noDebugger
eqeqeqsuspicious/noDoubleEquals
no-extra-boolean-castcomplexity/noExtraBooleanCast
react-hooks/exhaustive-depscorrectness/useExhaustiveDependencies
jsx-a11y/alt-texta11y/useAltText
jsx-a11y/no-autofocusa11y/noAutofocus
no-dangerously-set-inner-htmlsecurity/noDangerouslySetInnerHtml

VS Code Integration

// .vscode/settings.json
{
  // Disable ESLint and Prettier extensions for this project
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },

  // Disable conflicting extensions
  "[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
  "[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
  "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
  "[json]": { "editor.defaultFormatter": "biomejs.biome" }
}

Performance Comparison

# Real benchmark: 500 TypeScript files

# ESLint (JavaScript):
# Cold lint: 8.2 seconds
# With cache: 2.1 seconds

# Biome (Rust):
# Cold lint: 0.3 seconds  ← 27x faster
# Always cold (no cache needed — it's fast enough)
# Format: 0.1 seconds

# Why Biome is faster:
# 1. Written in Rust — 10-100x faster parser
# 2. Parallel file processing
# 3. No plugin overhead
# 4. No Node.js startup cost

Compare Biome and ESLint package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.