⚙️

monorepo (yarn workspace) で tsconfig や .eslintrc をいい感じに管理する

2022/08/24に公開

背景

monorepo で TypeScript のプロジェクトを構成するとき、設定を共通化する方法として root ディレクトリに tsconfig.base.json ファイルを置き "extends": "../../tsconfig.base.json" のように指定する方法がよく使われます。

ところが先日 Turborepo を導入したついでに Examples (以降、元ネタ) を眺めていたところ tsconfigeslint-config-custom という専用のパッケージを切って管理していたのを見つけました。

この方法をもとに、よりいい感じに管理するために少しアレンジしてみたので紹介します。

tsconfig をいい感じに管理する

tsconfig 専用のパッケージ追加

元ネタでは単に tsconfig というパッケージ名にしていますが、 npm パッケージではなくプロジェクト内参照であることがわかりやすいように @monorepo/tsconfig というように prefix をつけました。
この @monorepo の部分はプロジェクト名などに適宜読み変えてください。

今回は説明を簡単にするために tsconfig/bases を使用します。
tsconfig/bases の README には "extends": "@tsconfig/node16-strictest/tsconfig.json" のように書いていますが /tsconfig.json は省略可能なようです。

packages/tsconfig/package.json
{
  "name": "@monorepo/tsconfig",
  "private": true,
  "version": "0.0.0",
  "devDependencies": {
    "@tsconfig/next": "^1.0.2",
    "@tsconfig/node16-strictest": "^1.0.3"
  }
}
packages/tsconfig/tsconfig.json
{
  "extends": "@tsconfig/node16-strictest"
}

他のパッケージから参照する

共通の tsconfig を使用したいパッケージ(例えば、backend)の devDependencies に、作成した @monorepo/tsconfig への参照を追加します。

apps/backend/package.json
{
  "name": "backend",
  "private": true,
  "version": "0.0.0",
  "dependencies": {
    // ...
  },
  "devDependencies": {
    "@monorepo/tsconfig": "workspace:^",
    // ...
  }
}
apps/backend/tsconfig.json
{
  "extends": "@monorepo/tsconfig"
}

複数の設定を共通化する

例えば Next.js 用の tsconfig を作り frontend 用のパッケージに適用したい場合は @monorepo/tsconfig パッケージ内に nextjs/tsconfig.json を作り、それぞれ以下のように設定します。

packages/tsconfig/nextjs/tsconfig.json
{
  "extends": "@tsconfig/next"
}
apps/frontend/package.json
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "dependencies": {
    // ...
  },
  "devDependencies": {
    "@monorepo/tsconfig": "workspace:^",
    // ...
  }
}
apps/frontend/tsconfig.json
{
  "extends": "@monorepo/tsconfig/nextjs"
}

.eslintrc もいい感じに管理する

考え方は tsconfig の場合とほぼ同じなので詳しい説明は割愛しますが ESLint の Shareable Configs のドキュメントに従い、パッケージ名は @monorepo/eslint-config のようにすると良さそうです。

packages/eslint-config/package.json
{
  "name": "@monorepo/eslint-config",
  "private": true,
  "version": "0.0.0",
  "devDependencies": {
    "eslint-config-next": "^12.2.5",
    "eslint-config-prettier": "^8.5.0"
  }
}
packages/eslint-config/index.js
module.exports = {
  extends: ["eslint:recommended", "prettier"],
}
apps/backend/package.json
{
  "name": "backend",
  "private": true,
  "version": "0.0.0",
  "dependencies": {
    // ...
  },
  "devDependencies": {
    "@monorepo/eslint-config": "workspace:^",
    "@monorepo/tsconfig": "workspace:^",
    // ...
  }
}
apps/backend/.eslintrc.js
module.exports = {
  "extends": ["@monorepo/eslint-config"]
}

この方法のメリット

../../ のような相対パスの指定がなくなりスッキリする

この方法を採用した最初のモチベーションはこれでした。
単に見た目がスッキリするだけではなく、ディレクトリ構造の階層が変わった場合でも書き換える必要がなくなります。

複数の共通設定が管理しやすい

root ディレクトリに tsconfig.base.json などを置く方法だと、今回のように Next.js 用の設定を作りたいとなった場合に root ディレクトリにファイルがどんどん増えるか、新たにディレクトリを切っていく必要が出てきます。
最初から専用のパッケージを切っておけば、複数の設定があってもきれいに管理できます。

eslint-config-* などの依存を root の package.json に追加する必要がなくなる

上でみた例のように tsconfig/baseseslint-config-* の依存は各 @monorepo/tsconfig@monorepo/eslint-config 側に追加していくため root の package.json が肥大化していくのを防ぐことができます。

まとめ

monorepo の設定方法としてバックエンドとフロントエンドを一つのリポジトリにまとめるようなものはたくさん情報があるのですが、私たちのプロジェクトプロジェクトでは複数の Next.js や React Native のアプリケーションを一つのリポジトリにまとめておりそこそこ規模の大きいコードベースになってきたため、ディレクトリ構成なども考え直す必要が出てきました。

そんな中で Turborepo の Examples は参考にすることが多く、例えば今回のコード例でも appspackages を分けているディレクトリ構成も真似してみました。

monorepo で開発している場合は Turborepo を導入していなくても一度目を通してみると発見があるかもしれません。

Discussion