pnpm + Turborepo で Nuxt アプリの monorepo 開発環境をつくる
- 投稿した日
 - 更新した日
 - 書いたひと
 - ひらたけ

 
私のこのウェブサイトをつくるときに、pnpm と Turborepo を使って Nuxt の monorepo 環境を構築したので、備忘録的に残しておこうと思います。
手っ取り早く環境をつくりたい
テンプレートリポジトリをつくりました。 GitHub でリポジトリを開いて「Use this template」から利用できます ので、もしよろしければ使ってください。
もし不具合とか追加で設定しておいたほうがよさげなものとかあれば教えてください🙏
環境
今回作業した環境は以下のとおりです。
ディレクトリ構造
最終的な monorepo 開発環境のディレクトリ構造は以下のとおりです(上にある テンプレートリポジトリ とだいたい一緒です)。
monorepo/
 ├ app/
 │ ├ public/
 │ ├ server/
 │ ├ .eslintrc.js
 │ ├ app.vue
 │ ├ nuxt.config.ts
 │ ├ package.json
 │ └ tsconfig.json
 ├ packages/
 │ └ eslint-config-custom/
 │   ├ index.js
 │   └ package.json
 ├ .gitignore
 ├ .npmrc
 ├ package.json
 ├ pnpm-lock.yaml
 ├ pnpm-workspace.yaml
 └ turbo.json
Nuxt アプリケーションは app/ 以下にあります。また、Turborepo が用意している例 を参考に、packages/ に ESLint の設定をまとめたパッケージをつくっています。
今回の説明では packages/ の中にあるパッケージは 1 つだけですが、任意のパッケージを複数入れることができます。
monorepo のベースを作成
monorepo 開発環境のベースをつくります。新しくリポジトリをつくり、以下のコマンドまたは手動でリポジトリのルートに package.json を作成します。
$ pnpm init
続いて、pnpm のワークスペースの設定をしていきます。
npm や yarn では package.json に workspaces というプロパティを追加して設定しますが、pnpm では pnpm-workspace.yaml というファイルをリポジトリのルートにつくり、そこに設定を書きます。今回は app/ と、packages/ の中にある各ディレクトリを指定します。
packages:
  - 'app'
  - 'packages/*'
Turborepo のインストール
Turborepo は、Next.js などを開発している Vercel 社がつくっているビルドシステムです。
今回の例ではあまり当てはまりませんが、複雑なウェブアプリケーションで、monorepo にたくさんのパッケージを追加し、それぞれのパッケージが同じ monorepo 内のパッケージを利用する場合に 「このアプリケーションは ○○ というパッケージのビルドを実行してからじゃないと動かすことが出来ない」 といったことが発生します。
また、たくさんのパッケージがあると、monorepo 全体のビルドを実行した場合に 何も変更をしていないパッケージまでビルドが実行されてしまい無駄な時間がかかってしまう 、などの問題も起こってしまいます。
そのあたりのいろいろを、いい感じにしてくれるのが Turborepo です。
以下のコマンドを実行して、Turborepo をインストールします。
$ pnpm add -wD turbo
pnpm のワークスペース機能をつかう場合、ルートのプロジェクトにパッケージをインストールする場合に -w オプションをつけないと警告が表示されインストールができません。
個人的には、「うっかりルートのプロジェクトにパッケージをインストールしちゃった!」ということが多いので、オプションをつけないとインストールできないというのは助かっています。
インストールが完了したら、Turborepo の設定ファイル turbo.json をリポジトリのルートに作成します。
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {}
}
Turborepo では .turbo というディレクトリが作成され、その中にログファイルなどがつくられます。なので、このディレクトリを .gitignore へ忘れずに追加するようにしましょう。
Nuxt アプリケーションを作成
Nuxt のアプリケーションをつくります。今回の monorepo 構成では app/ に Nuxt アプリケーションを置くことになっているので、まずはこのディレクトリを作成します。
ディレクトリを作成したら、そのディレクトリへ移動して新規 Nuxt アプリケーションを作成します。私はいつも公式ドキュメントのコマンドを実行して、全ファイルをカレントディレクトリへ移動してつくっています。
$ cd app
$ npx nuxi@latest init nuxt-app
$ mv nuxt-app/* .
新しく Nuxt アプリケーションを作成すると、.gitignore や .npmrc がつくられます。これらは、リポジトリのルートに移動するか、既にルートにあるファイルに内容を追加するなどしてください。
アプリケーションを作成できたら、リポジトリのルートへ戻ります。ルートの package.json の scripts に build スクリプトを追加します。
{
  "scripts": {
    "build": "turbo run build" // 👈これを追加
  },
  ...
}
turbo run build は、Turborepo を使用して monorepo にある全てのパッケージの build スクリプトを実行することができるコマンドです。今回の例では app/ にある Nuxt アプリケーションの package.json の scripts にある build スクリプトが実行されます。
packages/ の中にあるパッケージにも、package.json の scripts に build スクリプトが設定されているものがあれば、Nuxt アプリケーションの build スクリプトと一緒にまとめて実行することができます。
この turbo run build の build の部分は、他のスクリプトを指定することも可能です。この後で設定しますが、turbo run lint で全てのパッケージの lint コマンドをまとめて実行することができます。 ただし、指定できるスクリプトは turbo.json の pipeline に設定を記述したスクリプトのみです 。
build スクリプトについて、turbo.json の pipeline に設定を追加します。この設定を追加することで、turbo run build を実行することができるようになります。
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/*", ".output/*", ".nuxt/*"]
    },
  }
}
dependsOn に指定している ["^build"] は、Turborepo に依存関係を考慮してタスクを実行するように伝えるものです。outputs には、2 回目以降に実行する際にキャッシュしておきたいもの(ビルドで生成されるファイルなど)を指定します。
pipeline の設定方法の詳細は、Turborepo のドキュメント をご確認ください。
ここまで設定した状態で、リポジトリのルートで build スクリプトを実行すると、Nuxt アプリケーションのビルドを Turborepo で実行することができます。
$ pnpm build
ESLint の設定を追加
ESLint の設定を追加します。
monorepo の場合複数のパッケージやアプリケーションがあるため、それぞれで異なるルールを設定することになると思います。けれども、全てが完全に違うルールというわけでもないと思います。
- Prettier を導入しているので eslint-config-prettier は全てのプロジェクトで設定する
 - React と Vue.js のプロジェクトがそれぞれ複数あり、React のプロジェクトでは React 用の共通ルール、Vue.js のプロジェクトでは Vue.js 用の共通ルールを設定する
 
こういった場合に、ESLint のルールをまとめたパッケージを monorepo 内に用意し、共通で使えるようにしておくと便利です。ESLint の設定を共有する方法についての詳しい説明については、以下の公式ドキュメントをお読みください。
今回は Nuxt アプリケーションだけしかないので、 Nuxt 用の共通で使える ESLint 設定のパッケージ をつくります。
まず、packages/ の中に eslint-config-custom ディレクトリを作成し、コマンドまたは手動で package.json を作成します。
{
  "name": "eslint-config-custom",
  "version": "0.0.0",
  "private": true,
  "main": "index.js"
}
ESLint のドキュメント にもありますが、実際に設定を使うときに eslint-config-custom の eslint-config- の部分を省略できるため、eslint-config-<任意の名前> という名前でパッケージをつくると便利です。
次に、共通で使用する ESLint の設定をつくります。
Nuxt には @nuxt/eslint-config という設定があるため、これを使用します。
$ cd packages/eslint-config-custom
$ pnpm add @nuxt/eslint-config
先ほどの package.json で main に指定した index.js に設定を書いていきます。もし追加で設定しておきたいルールがあれば rules に追加します。
module.exports = {
  extends: ['@nuxt/eslint-config'],
  rules: {
    // 任意のルールを追加
  },
}
これで共通で使用する ESLint の設定ができたので、さっそく Nuxt アプリケーションにインストールしていきます。app/ へ移動して、ESLint と eslint-config-custom をインストールします。
このとき、 確実にワークスペース内の他のパッケージをインストールできるよう --workspace オプションをつけることをおすすめします 。--workspace オプションは、ワークスペースで見つかった場合にのみ指定したパッケージをインストールするオプションです。同じ名前のパッケージが一般公開されていた場合に、そのパッケージをインストールしてしまうのを防ぐことができます。
入れるつもりのなかったパッケージをインストールしてしまい、危険なプログラムを実行してしまう可能性もあるので、念のためオプションをつけておいたほうがいいかな、と思います。
$ cd ../../app
$ pnpm add -D eslint
$ pnpm add -D eslint-config-custom --workspace
ESLint の設定ファイル .eslintrc.js を作成し、インストールした設定を使用するように指定します。先ほど説明したように、eslint-config- の部分は省略できます。
module.exports = {
  root: true,
  extends: ['custom'],
}
設定ファイルが作成できたら、Nuxt アプリケーションの package.json の scripts に ESLint を実行するスクリプトを追加し、実際に動作することを確認します。
{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "lint": "eslint . --ext .js,.ts,.vue" // 👈これを追加
  },
  ...
}
最後に、リポジトリのルートに戻り turbo.json に lint スクリプトの設定を追加します。
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/*", ".output/*", ".nuxt/*"]
    },
    "lint": {} // 👈これを追加
  }
}
これで今回の環境構築は完了です。おつかれさまでした。