もふもふ技術部

IT技術系mofmofメディア

既存プロジェクトでPrettier・ESLintをBiomeに移行してみた

Biome v1.7からPrettierとESLintから移行するためのコマンドが追加されたので、既存のNext.jsプロジェクトで試しに移行コマンドを叩いてみました。

biomejs.dev

Biomeのインストールと初期設定

Biomeをインストール。

> npm i --save-dev --save-exact @biomejs/biome@latest

added 3 packages, removed 81 packages, and audited 891 packages in 5s

157 packages are looking for funding
  run `npm fund` for details

3 vulnerabilities (2 moderate, 1 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

設定ファイル生成コマンドを実行する。

> npx @biomejs/biome init   

Welcome to Biome! Let's get you started...

Files created 

  - biome.json
    Your project configuration. See https://biomejs.dev/reference/configuration

Next Steps 

  1. Setup an editor extension
     Get live errors as you type and format when you save.
     Learn more at https://biomejs.dev/guides/integrate-in-editor/

  2. Try a command
     biome check  checks formatting, import sorting, and lint rules.
     biome --help displays the available commands.

  3. Migrate from ESLint and Prettier
     biome migrate eslint   migrates your ESLint configuration to Biome.
     biome migrate prettier migrates your Prettier configuration to Biome.

  4. Read the documentation
     Find guides and documentation at https://biomejs.dev/guides/getting-started/

  5. Get involved with the community
     Ask questions and contribute on GitHub: https://github.com/biomejs/biome
     Seek for help on Discord: https://discord.gg/BypW39g6Yc

biome.jsonという設定ファイルが生成される。デフォルトはインデントが深いです。 (--jsoncオプションをつければjsoncファイルでも生成できます)

// biome.json
{
    "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
    "organizeImports": {
        "enabled": true
    },
    "linter": {
        "enabled": true,
        "rules": {
            "recommended": true
        }
    }
}

ESLintからの移行コマンド

実行するとエラーが出ましたが、今回は無視して進めます。

>  npx biome migrate eslint --write
migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Migration has encountered an error: The module '/app/node_modules/eslint-config-next/index.js' cannot be loaded. Make sure that the module exists.
  

.eslintignore has been successfully migrated.
./.eslintrc.js has been successfully migrated.
Run the command with the option --include-inspired to also migrate inspired rules.
// biome.json
{
    "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
    "organizeImports": { "enabled": true },
    "linter": {
        "enabled": true,
        "rules": {
            "recommended": false,
            "complexity": {
                "noBannedTypes": "error",
                "noExtraBooleanCast": "error",
                "noMultipleSpacesInRegularExpressionLiterals": "error",
                "noUselessCatch": "error",
                "noUselessTypeConstraint": "error",
                "noWith": "error"
            },
            "correctness": {
                "noConstAssign": "error",
                "noConstantCondition": "error",
                "noEmptyCharacterClassInRegex": "error",
                "noEmptyPattern": "error",
                "noGlobalObjectCalls": "error",
                "noInnerDeclarations": "error",
                "noInvalidConstructorSuper": "error",
                "noNewSymbol": "error",
                "noNonoctalDecimalEscape": "error",
                "noPrecisionLoss": "error",
                "noSelfAssign": "error",
                "noSetterReturn": "error",
                "noSwitchDeclarations": "error",
                "noUndeclaredVariables": "error",
                "noUnreachable": "error",
                "noUnreachableSuper": "error",
                "noUnsafeFinally": "error",
                "noUnsafeOptionalChaining": "error",
                "noUnusedLabels": "error",
                "noUnusedVariables": "error",
                "useIsNan": "error",
                "useValidForDirection": "error",
                "useYield": "error"
            },
            "style": {
                "noNamespace": "error",
                "useAsConstAssertion": "error",
                "useBlockStatements": "off"
            },
            "suspicious": {
                "noAsyncPromiseExecutor": "error",
                "noCatchAssign": "error",
                "noClassAssign": "error",
                "noCompareNegZero": "error",
                "noControlCharactersInRegex": "error",
                "noDebugger": "error",
                "noDuplicateCase": "error",
                "noDuplicateClassMembers": "error",
                "noDuplicateObjectKeys": "error",
                "noDuplicateParameters": "error",
                "noEmptyBlockStatements": "error",
                "noExplicitAny": "error",
                "noExtraNonNullAssertion": "error",
                "noFallthroughSwitchClause": "error",
                "noFunctionAssign": "error",
                "noGlobalAssign": "error",
                "noImportAssign": "error",
                "noMisleadingCharacterClass": "error",
                "noMisleadingInstantiator": "error",
                "noPrototypeBuiltins": "error",
                "noRedeclare": "error",
                "noShadowRestrictedNames": "error",
                "noUnsafeDeclarationMerging": "error",
                "noUnsafeNegation": "error",
                "useAwait": "error",
                "useGetterReturn": "error",
                "useValidTypeof": "error"
            }
        },
        "ignore": ["src/generated/*", "src/**/*.d.ts"]
    },
    "overrides": [
        {
            "include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
            "linter": {
                "rules": {
                    "correctness": {
                        "noConstAssign": "off",
                        "noGlobalObjectCalls": "off",
                        "noInvalidConstructorSuper": "off",
                        "noNewSymbol": "off",
                        "noSetterReturn": "off",
                        "noUndeclaredVariables": "off",
                        "noUnreachable": "off",
                        "noUnreachableSuper": "off"
                    },
                    "style": {
                        "noArguments": "error",
                        "noVar": "error",
                        "useConst": "error"
                    },
                    "suspicious": {
                        "noDuplicateClassMembers": "off",
                        "noDuplicateObjectKeys": "off",
                        "noDuplicateParameters": "off",
                        "noFunctionAssign": "off",
                        "noImportAssign": "off",
                        "noRedeclare": "off",
                        "noUnsafeNegation": "off",
                        "useGetterReturn": "off"
                    }
                }
            }
        },
        {
            "include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
            "linter": {
                "rules": {
                    "correctness": {
                        "noConstAssign": "off",
                        "noGlobalObjectCalls": "off",
                        "noInvalidConstructorSuper": "off",
                        "noNewSymbol": "off",
                        "noSetterReturn": "off",
                        "noUndeclaredVariables": "off",
                        "noUnreachable": "off",
                        "noUnreachableSuper": "off"
                    },
                    "style": {
                        "noArguments": "error",
                        "noVar": "error",
                        "useConst": "error"
                    },
                    "suspicious": {
                        "noDuplicateClassMembers": "off",
                        "noDuplicateObjectKeys": "off",
                        "noDuplicateParameters": "off",
                        "noFunctionAssign": "off",
                        "noImportAssign": "off",
                        "noRedeclare": "off",
                        "noUnsafeNegation": "off",
                        "useGetterReturn": "off"
                    }
                }
            }
        }
    ]
}

Prettierからの移行コマンド

> npx biome migrate prettier --write
.prettierignore has been successfully migrated.
./prettier.config.js has been successfully migrated.
// biome.json
{
    "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
    "formatter": {
        "enabled": true,
        "formatWithErrors": false,
        "indentStyle": "space",
        "indentWidth": 2,
        "lineEnding": "lf",
        "lineWidth": 80,
        "attributePosition": "auto",
        "ignore": ["src/generated/*"]
    },
    "organizeImports": { "enabled": true },
    "linter": {
        "enabled": true,
        "rules": {
            "recommended": false,
            "complexity": {
                "noBannedTypes": "error",
                "noExtraBooleanCast": "error",
                "noMultipleSpacesInRegularExpressionLiterals": "error",
                "noUselessCatch": "error",
                "noUselessTypeConstraint": "error",
                "noWith": "error"
            },
            "correctness": {
                "noConstAssign": "error",
                "noConstantCondition": "error",
                "noEmptyCharacterClassInRegex": "error",
                "noEmptyPattern": "error",
                "noGlobalObjectCalls": "error",
                "noInnerDeclarations": "error",
                "noInvalidConstructorSuper": "error",
                "noNewSymbol": "error",
                "noNonoctalDecimalEscape": "error",
                "noPrecisionLoss": "error",
                "noSelfAssign": "error",
                "noSetterReturn": "error",
                "noSwitchDeclarations": "error",
                "noUndeclaredVariables": "error",
                "noUnreachable": "error",
                "noUnreachableSuper": "error",
                "noUnsafeFinally": "error",
                "noUnsafeOptionalChaining": "error",
                "noUnusedLabels": "error",
                "noUnusedVariables": "error",
                "useIsNan": "error",
                "useValidForDirection": "error",
                "useYield": "error"
            },
            "style": {
                "noNamespace": "error",
                "useAsConstAssertion": "error",
                "useBlockStatements": "off"
            },
            "suspicious": {
                "noAsyncPromiseExecutor": "error",
                "noCatchAssign": "error",
                "noClassAssign": "error",
                "noCompareNegZero": "error",
                "noControlCharactersInRegex": "error",
                "noDebugger": "error",
                "noDuplicateCase": "error",
                "noDuplicateClassMembers": "error",
                "noDuplicateObjectKeys": "error",
                "noDuplicateParameters": "error",
                "noEmptyBlockStatements": "error",
                "noExplicitAny": "error",
                "noExtraNonNullAssertion": "error",
                "noFallthroughSwitchClause": "error",
                "noFunctionAssign": "error",
                "noGlobalAssign": "error",
                "noImportAssign": "error",
                "noMisleadingCharacterClass": "error",
                "noMisleadingInstantiator": "error",
                "noPrototypeBuiltins": "error",
                "noRedeclare": "error",
                "noShadowRestrictedNames": "error",
                "noUnsafeDeclarationMerging": "error",
                "noUnsafeNegation": "error",
                "useAwait": "error",
                "useGetterReturn": "error",
                "useValidTypeof": "error"
            }
        },
        "ignore": ["src/generated/*", "src/**/*.d.ts"]
    },
    "javascript": {
        "formatter": {
            "jsxQuoteStyle": "double",
            "quoteProperties": "asNeeded",
            "trailingComma": "es5",
            "semicolons": "asNeeded",
            "arrowParentheses": "always",
            "bracketSpacing": true,
            "bracketSameLine": false,
            "quoteStyle": "single",
            "attributePosition": "auto"
        }
    },
    "overrides": [
        {
            "include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
            "linter": {
                "rules": {
                    "correctness": {
                        "noConstAssign": "off",
                        "noGlobalObjectCalls": "off",
                        "noInvalidConstructorSuper": "off",
                        "noNewSymbol": "off",
                        "noSetterReturn": "off",
                        "noUndeclaredVariables": "off",
                        "noUnreachable": "off",
                        "noUnreachableSuper": "off"
                    },
                    "style": {
                        "noArguments": "error",
                        "noVar": "error",
                        "useConst": "error"
                    },
                    "suspicious": {
                        "noDuplicateClassMembers": "off",
                        "noDuplicateObjectKeys": "off",
                        "noDuplicateParameters": "off",
                        "noFunctionAssign": "off",
                        "noImportAssign": "off",
                        "noRedeclare": "off",
                        "noUnsafeNegation": "off",
                        "useGetterReturn": "off"
                    }
                }
            }
        },
        {
            "include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
            "linter": {
                "rules": {
                    "correctness": {
                        "noConstAssign": "off",
                        "noGlobalObjectCalls": "off",
                        "noInvalidConstructorSuper": "off",
                        "noNewSymbol": "off",
                        "noSetterReturn": "off",
                        "noUndeclaredVariables": "off",
                        "noUnreachable": "off",
                        "noUnreachableSuper": "off"
                    },
                    "style": {
                        "noArguments": "error",
                        "noVar": "error",
                        "useConst": "error"
                    },
                    "suspicious": {
                        "noDuplicateClassMembers": "off",
                        "noDuplicateObjectKeys": "off",
                        "noDuplicateParameters": "off",
                        "noFunctionAssign": "off",
                        "noImportAssign": "off",
                        "noRedeclare": "off",
                        "noUnsafeNegation": "off",
                        "useGetterReturn": "off"
                    }
                }
            }
        }
    ]
}

package.jsonのscript変更

// package.json
{
  "name": "app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "tsc": "tsc --noEmit",
-    "lint": "next lint",
-    "prettier": "prettier --check .",
-    "format": "next lint --fix && prettier --write .",
+    "lint": "biome lint --apply .",
+    "format": "biome format .",
+    "check": "biome check --apply .",
  },
  ...
}

実行してみる

体感できるくらい爆速になりました!

ESLintの移行コマンド実行時にエラーが出てしまいましたが、基本的にmigrateコマンドを実行するだけで簡単に移行できました。

> npm run check
...
Skipped 39 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --apply-unsafe

The number of diagnostics exceeds the number allowed by Biome.
Diagnostics not shown: 327.
Checked 272 files in 163ms. Fixed 58 files.
Found 27 errors.
check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ Some errors were emitted while applying fixes.