💫

モノレポ(NestJS)でBiomeをセットアップする

2024/07/01に公開

ESLint, Prettierをいちいち設定するのが嫌(というか面倒)なので、Biomeを導入してみました。

まだNestJSベースのAPIサーバーしか配置していないのですが、モノレポ前提で設定してみたので、モジュールが増えても対応できると思います。

モノレポでのBiomeのセットアップ

Biomeの公式ドキュメントにモノレポについての記載があり、ちょっと制限はあるようですが、問題があったら変えればいいやの精神でやっていきます。

In order to have the best developer experience despite the current limitation, it’s advised to have a biome.json at the root of the monorepo, and use the overrides configuration to change the behaviour of Biome in certain packages.

まずモノレポにおいてBiomeをどこにインストールするか?という疑問が湧きますが、公式の推奨通り、ルートにインストールします。

npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init

作成されたbiome.jsonに基本的な設定を書きます。一見長く見えますが、ほとんど基本的な設定しかしてません。overridesの部分については後述します。

biome.json
{
  "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "ignore": [],
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "json": {
    "parser": {
      "allowComments": true
    },
    "formatter": {
      "enabled": true,
      "indentStyle": "space",
      "indentWidth": 2,
      "lineWidth": 100
    }
  },
  "javascript": {
    "formatter": {
      "enabled": true,
      "quoteStyle": "single",
      "jsxQuoteStyle": "double",
      "trailingCommas": "all",
      "indentStyle": "space",
      "indentWidth": 2,
      "lineWidth": 100
    }
  },
  "overrides": [
    {
      "include": ["api/**"],
      "linter": {
        "rules": {
          "style": {
            "useImportType": "off",
            "useNodejsImportProtocol": "off"
          }
        }
      }
    }
  ]
}

VSCodeの拡張機能をインストール。extensions.jsonとsettings.jsonも設定しておきます。

一応デフォルトフォーマッタはPrettierを残してますが、Biomeに寄せても良さそうならBiomeにしてしまいたいところ。ここは様子見。

.vscode/extensions.json
{
  "recommendations": ["biomejs.biome", "esbenp.prettier-vscode"]
}
.vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  }
}

次にモノレポの各モジュールです。私はapiというフォルダを切って、そこにNestJSでAPIサーバーを構築しています。まずはnpm run xxxスクリプトでBiomeを使うように修正します。

api/package.json
  "scripts": {
-    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
-    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
+    "format": "biome format --write",
+    "lint": "biome lint --write",
  },

はじめは"format": "biome format --write \"src/**/*.ts\" \"test/**/*.ts\""のように単純にBiomeを使うように変更したんですが、以下のエラーを解消できませんでした。

> biome format --write "src/**/*.ts" "test/**/*.ts"

src/**/*.ts internalError/io  INTERNAL  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ No such file or directory (os error 2)
  
  ⚠ This diagnostic was derived from an internal Biome error. Potential bug, please report it if necessary.
  

test/**/*.ts internalError/io  INTERNAL  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ No such file or directory (os error 2)
  
  ⚠ This diagnostic was derived from an internal Biome error. Potential bug, please report it if necessary.
  

Formatted 0 files in 297µs. No fixes applied.
internalError/io ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ✖ No files were processed in the specified paths.

そこで、api/package.jsonは先述の通りただ"format": "biome format --write"とし、api/biome.jsonを作成して対象のファイルを指定しました。

api/biome.json
{
  "extends": ["../biome.json"],
  "linter": {
    "include": ["src/**/*.ts", "apps/**/*.ts", "libs/**/*.ts", "test/**/*.ts"]
  },
  "formatter": {
    "include": ["src/**/*.ts", "test/**/*.ts"]
  }
}

extendsこちらに記載があるとおり、ルートのbiome.jsonを拡張するものです。

ルートのbiome.jsonでincludeを使っても同様のことができそうです。このあたりは、今後モジュールが増えていったときにどちらがよいか判断したいと思います。

また、これでESLintとPrettierは不要なので外します(もともと入っていたので)。

  • ESLint, Prettier関連ライブラリをアンインストール
  • eslintrc, prettierrcなど設定ファイルを削除

NestJSでのBiomeのセットアップ

モノレポでのセットアップはここまでなんですが、NestJSでBiomeを使ったときに問題が出たので記載します。

NestJSが依存関係の解決に失敗する

npm run lintしたあとにnpm run start:devで起動しようとすると、以下のエラーが発生しました。

[Nest] 83612  - 07/01/2024, 8:01:59 PM     LOG [NestFactory] Starting Nest application...
[Nest] 83612  - 07/01/2024, 8:01:59 PM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +15ms
[Nest] 83612  - 07/01/2024, 8:01:59 PM   ERROR [ExceptionHandler] Nest can't resolve dependencies of the AppController (?). Please make sure that the argument Function at index [0] is available in the AppModule context.

Potential solutions:
- Is AppModule a valid NestJS module?
- If Function is a provider, is it part of the current AppModule?
- If Function is exported from a separate @Module, is that module imported within AppModule?
  @Module({
    imports: [ /* the Module containing Function */ ]
  })

Error: Nest can't resolve dependencies of the AppController (?). Please make sure that the argument Function at index [0] is available in the AppModule context.

Potential solutions:
- Is AppModule a valid NestJS module?
- If Function is a provider, is it part of the current AppModule?
- If Function is exported from a separate @Module, is that module imported within AppModule?
  @Module({
    imports: [ /* the Module containing Function */ ]
  })

怒られているAppControllerは以下。コンストラクタの0番目、つまりAppServiceの依存が解決できないようです。

api/src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import type { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

もともとここは以下のように、typeではなく値と型の両方をインポートするのが正しいです。

api/src/app.controller.ts
- import type { AppService } from './app.service';
+ import { AppService } from './app.service';

しかし、typeを外すと"All these imports are only used as types."と言われてしまいます。確かに型しか使ってないように見えるんですが、NestJSはコンパイル時に値としても使うため、import typeだと動かないんですよね...(Issue見つけた)

ということで、これを見逃してくれるようにuseImportTypeをオフにしました。ルートのbiome.jsonでapiフォルダをincludeして配下のルールを設定します。これも公式で案内されているスタイルです。

biome.json
...
  "overrides": [
    {
      "include": ["api/**"],
      "linter": {
        "rules": {
          "style": {
            "useImportType": "off",
            "useNodejsImportProtocol": "off"
          }
        }
      }
    }
  ]
...

Node.jsの標準モジュールをRequireさせてくる

今回はpathだったんですが、Node.jsの標準モジュールをimportしようとすると
"A Node.js builtin module should be imported with the node: protocol."と怒られました。

基本的にimport/exportで対応したいので、このルールも同様にオフにしました。上記のuseNodejsImportProtocolです。

また実装を進めていくうちに色々出てきそうな気もしますが、ひとまずかなりサッパリした設定でフォーマッタ・リンタが設定できて嬉しいです。また、依存パッケージが減ったのもとても嬉しいです。

わーい🙌

Discussion