🎃

Turborepoのチュートリアル

2023/05/04に公開

はじめに

Turborepoは、高速でスケーラブルなモノレポ(monorepo)ビルドシステムです。自身の知識の棚卸しを目的に記事化しました。

本記事では、以下について解説します。

  • モノレポとマルチレポの違い
  • Turborepo とは何か、どのような課題を解決しているのか
  • Turborepo を実際に利用するためのチュートリアル

Turborepo のチュートリアルは以下の公式チュートリアルをベースに、自身で足りないと思った要素を追加しています。

https://turbo.build/repo/docs/getting-started/create-new

マルチレポとは

モノレポ(monorepo)とマルチレポ(multirepo)は、プロジェクトのコード管理戦略の異なる形態です。

モノレポでは、すべてのプロジェクトやパッケージが 1 つのリポジトリで管理されるのに対し、マルチレポでは各プロジェクトやパッケージが独自のリポジトリで管理されます。

以下の図がマルチレポの構成を表した例です。Repo1、Repo2、Repo3 という 3 つのリポジトリが存在し、依存関係が存在しています。

開発体験に基づいた、マルチレポの主なメリットです。

項目 説明
スケーラビリティ 各リポジトリが独立しているため、プロジェクトが大規模になってもリポジトリのサイズが適切に保たれ、クローンやビルドの速度が維持されます。
権限管理 各プロジェクトごとにアクセス制御を容易に設定できます。
プロジェクトの独立性 各プロジェクトが独立しているため、開発やデプロイのサイクルが他のプロジェクトに影響を与えにくくなります。
シンプルなワークフロー プロジェクトごとにリポジトリが分割されているため、開発者は特定のプロジェクトに集中しやすくなります。

開発体験に基づいた、マルチレポの主なデメリットです。

項目 説明
一貫性の欠如 コーディングスタイルやツールの統一が難しくなることがあります。
依存関係の管理 パッケージ間の依存関係の追跡や更新が複雑になることがあります。
コード共有 共通のコードやリソースをパッケージ間で共有するのが難しくなり、重複が増える可能性があります。
統合テスト プロジェクト間の統合テストを実行するのが難しくなることがあります。
コードレビュー チームメンバーが複数のリポジトリの変更を追跡することが難しくなり、コードレビューが効率的でなくなることがあります。

モノレポとは

モノレポ(monorepo)は、すべてのプロジェクトやパッケージが 1 つのリポジトリで管理されるコード管理戦略です。

マルチレポと異なり、Repo という 1 つのリポジトリしかありません。依存関係は同じです。

開発体験に基づいた、モノレポの主なメリットです。

項目 説明
一貫性 全てのコードが同じリポジトリで管理されるため、コーディングスタイルやツールの統一が容易になります。
依存関係の管理 パッケージ間の依存関係を簡単に追跡し、更新することができます。
コード共有 共通のコードやリソースをパッケージ間で簡単に共有でき、重複を減らすことができます。
統合テスト 一元化されたリポジトリを使用することで、統合テストを容易に実行できます。
ワークフローの単純化 プロジェクト全体での変更やリファクタリングを行いやすくなります。
コードレビュー チームメンバーがリポジトリ全体の変更を追跡しやすくなり、コードレビューが効率的に行えます。

開発体験に基づいた、モノレポの主なデメリットです。

項目 説明
スケーラビリティ プロジェクトが大規模になると、リポジトリのサイズが大きくなり、クローンやビルドの速度が低下する可能性があります。
権限管理 モノレポでは、プロジェクトごとのアクセス制御が難しくなることがあります。
学習コスト モノレポの管理には、特定のツールやワークフローを習得する必要があります。

今回紹介する Turborepo は初期の学習コストは発生しますが、スケーラビリティを解決できます。具体的には、Turborepo では、リポジトリサイズが大きくなっても、ビルド時間が大きくなることはありません。

権限管理の課題は残るため、マルチレポとモノレポの選択は、プロジェクトの規模、チーム構成、開発プロセスなどの要素に基づいて判断する必要があります。

Turborepoとは

Turborepoは、高速でスケーラブルなモノレポ(monorepo)ビルドシステムです。あらためて、モノレポは、複数のプロジェクトやパッケージを 1 つのリポジトリで管理するコード管理戦略です。組織がコードの共有や再利用を効率的に行うことができます。しかし、モノレポは、ビルド時間の増加やデプロイの複雑さといった課題を抱えています。

Turborepo は、これらの課題へ対処するために設計されたビルドシステムで、以下のような機能を提供します。

項目 説明
キャッシュとインクリメンタルビルド Turborepoは、ビルドキャッシュを使用して、変更された部分だけを再ビルドします。これにより、ビルド時間が大幅に短縮され、開発の効率が向上します。
並列化 Turborepoは、複数のタスクを並行して実行することで、リント、ビルド、テストの速度を向上させます。
依存関係の自動解決 Turborepoは、プロジェクト間の依存関係を自動的に解決し、必要なタスクを適切な順序で実行します。
柔軟な設定 Turborepoは、プロジェクト固有の設定や共通の設定を簡単に管理できるように設計されており、開発者が効率的に作業できる環境を提供します。

なお、TypeScript 及び JavaScript のモノレポ環境に特化したツールで、Next.js を提供している Vercel によって開発されてます。

Turborepo は、モノレポを使用している開発者や組織にとって、リント、ビルド、テストの速度を向上させる有益なツールです。開発の効率を向上させたいモノレポの利用者には、Turborepo の導入を検討する価値があります。

以下は、Turborepo 公式サイト、Turbo GitHub です。

https://turbo.build/repo

https://github.com/vercel/turbo

Turborepoの追加説明

Turborepo が提供する機能の簡易説明をします。

タスクの並列実行

Turborepo が、複数のタスクを並行して実行するによる、実行時間の削減します。

例えば、3 つのリソースがあるとします。これらの lintbuildtest を実行する場合は、lintbuildtest の順に一連の流れで処理を行います。

Turborepo を利用すると、各リソースの依存関係が自動的に解決され、必要なタスクが適切な順序で実行されます。これにより、タスクを実行する時間が大幅圧縮されます。例として、web と docs の build は ui に依存しているため、ui の build が完了するまで実行されません。

ローカルキャッシュを利用したインクリメンタルビルド

Turborepo のローカルキャッシュを利用することで、変更された部分だけを再ビルドします。これにより、ビルド時間が大幅に短縮され、開発の効率が向上します。

具体的な利用シナリオシナリオを見ていきます。

  1. 貴方は開発者で、ソースコードを更新したとします。貴方は、ビルドが通るか、turbo run build をローカルで実行します。Turborepo は、ビルドのタスクの入力(つまりソースコードなどから)、ハッシュ値を算出します(例:f3adfa123a)

  1. 次に、ハッシュ値がローカルファイルシステム(例:/node_modules/.cache/turbo/)に存在するか確認します。

  1. Turborepo は、ハッシュが見つからない場合、ローカルでタスクを実行します。

  1. タスクが完了すると、Turborepo は実行結果をアーティファクトとしてハッシュ値に紐付いてローカルファイルシステムのキャッシュに保存します。

次に、ファイルに何も変更せずに、貴方は再度、ビルドのタスクを実行します。

  1. Turborepo は、ビルドのタスクの入力(つまりソースコードなどから)、ハッシュ値を算出します(例:f3adfa123a)

  1. 次に、ハッシュ値がローカルファイルシステム(例:/node_modules/.cache/turbo/)に存在するか確認します。

  1. 今回はキャッシュが存在したため、タスクを再度実行するのではなく、Turborepo は以前のアーティファクトを使うことで、時間とコンピュータリソースを節約します。

リモートキャッシュを利用したインクリメンタルビルド

Turborepo のリモートキャッシュを利用することで、チームメンバーがビルドした結果を共有し、ビルド時間を短縮します。

  1. 例えば、リモートキャッシュにキャッシュ化されていないタスクをまず実行します。

  1. 実行されたタスクの結果は、リモートキャッシュに保存されます。

  1. 他のチームメンバーが同一の内容をビルドする際には、リモートキャッシュからデータが取得され、ビルド時間が短縮されます。

なお、デプロイ先に Vercel を利用している場合、このリモートキャッシュは自動的に有効化されます。

事前準備

では、実際に Turborepo を利用していきます。まずは、事前準備として、以下のツールをインストールします。

turbo

Turborepo を利用するために、turbo コマンドを利用できるようにします。turbo をグローバルインストールする場合は、以下のコマンドを実行します。

$ pnpm install turbo --global

バージョンを確認します。

$ turbo --version
1.8.3

その他の詳細や、ローカルにインストールする場合は、Turborepo 公式のインストールガイドを参照ください。

https://turbo.build/repo/docs/installing

新規プロジェクト作成

create-turbo コマンドを利用し、新規に Turborepo プロジェクトを作成します。

$ pnpm dlx create-turbo@latest
実行ログ
>>> TURBOREPO

>>> Welcome to Turborepo! Let's get you set up with a new codebase.

? Where would you like to create your turborepo? ./my-turborepo
? Which package manager do you want to use? pnpm

Downloading files. This might take a moment.

>>> Created a new Turborepo with the following:

apps
 - apps/docs
 - apps/web
packages
 - packages/eslint-config-custom
 - packages/tsconfig
 - packages/ui

Installing packages. This might take a couple of minutes.

>>> Success! Created a new Turborepo at "my-turborepo".
Inside that directory, you can run several commands:

  pnpm run build
     Build all apps and packages

  pnpm run dev
     Develop all apps and packages

  pnpm run lint
     Lint all apps and packages

Turborepo will cache locally by default. For an additional
speed boost, enable Remote Caching with Vercel by
entering the following command:

  pnpm dlx turbo login

We suggest that you begin by typing:

  cd my-turborepo
  pnpm dlx turbo login

今回は、パッケージ管理に pnpm を選択しました。pnpm 以外に、yarnnpm を利用できますが、pnpm を公式は推奨しています

If you're not sure, we recommend choosing pnpm. If you don't have it installed, cancel create-turbo (via ctrl-C) and take a look at the installation instructions(opens in a new tab).

https://turbo.build/repo/docs/getting-started/create-new#which-package-manager-do-you-want-to-use

プロジェクト名は my-turborepo としています。作成したプロジェクトのディレクトリに移動します。

$ cd my-turborepo

プロジェクト構成を確認

プロジェクトの構成をツリー形式で表示します。
node_modules, .git, .next は表示しないようにしています。

$ tree -I node_modules -I .git -I .next --dirsfirst -a
フォルダ構成
.
├── apps
│   ├── docs
│   │   ├── pages
│   │   │   └── index.tsx
│   │   ├── .eslintrc.js
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── next-env.d.ts
│   │   ├── next.config.js
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── web
│       ├── pages
│       │   └── index.tsx
│       ├── .eslintrc.js
│       ├── .gitignore
│       ├── README.md
│       ├── next-env.d.ts
│       ├── next.config.js
│       ├── package.json
│       └── tsconfig.json
├── packages
│   ├── eslint-config-custom
│   │   ├── index.js
│   │   └── package.json
│   ├── tsconfig
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   ├── package.json
│   │   └── react-library.json
│   └── ui
│       ├── Button.tsx
│       ├── index.tsx
│       ├── package.json
│       └── tsconfig.json
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── README.md
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

全体構成

プロジェクトの構成は大きく3つに別れています。

要素 説明
apps 独立して動作するアプリケーションが配置
packages プロジェクトで共有で利用するプログラムや設定が配置
ルート プロジェクト全体の管理に必要となる設定が配置

apps

apps には、独立して動作するアプリケーションとして webdocs はに配置されています。それぞれがワークスペースに該当し、計 2 つのワークスペースが apps に含まれます。ワークスペースとは何かは後ほど解説します。

ワークスペース名 パス 説明
web apps/web TypeScriptのNext.jsアプリ
docs apps/docs TypeScriptのNext.jsアプリ

webdocs は同じ構成のため、web に該当することは docs についても同様のことが言えると理解してください。

apps構成
.
└── apps
    ├── docs
    │   ├── pages
    │   │   └── index.tsx
    │   ├── .eslintrc.js
    │   ├── .gitignore
    │   ├── README.md
    │   ├── next-env.d.ts
    │   ├── next.config.js
    │   ├── package.json
    │   └── tsconfig.json
    └── web
        ├── pages
        │   └── index.tsx
        ├── .eslintrc.js
        ├── .gitignore
        ├── README.md
        ├── next-env.d.ts
        ├── next.config.js
        ├── package.json
        └── tsconfig.json

packages

packages には、他のワークスペースで利用するプログラムや設定として uieslint-config-customtsconfig が配置されています。それぞれがワークスペースに該当し、計 3 つのワークスペースが packages に含まれます。ワークスペースとは何かは後ほど解説します。

ワークスペース名 パス 説明
ui packages/ui 共有のReactコンポーネント
eslint-config-custom packages/eslint-config-custom 共有のESLint設定
tsconfig packages/tsconfig 共有のTypeScriptの設定
packages構成
.
└── packages
    ├── eslint-config-custom
    │   ├── index.js
    │   └── package.json
    ├── tsconfig
    │   ├── base.json
    │   ├── nextjs.json
    │   ├── package.json
    │   └── react-library.json
    └── ui
        ├── Button.tsx
        ├── index.tsx
        ├── package.json
        └── tsconfig.json

ルート

ルートにプロジェクト全体の管理に必要となる設定が配置されています。

ルート構成
.
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── README.md
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
ファイル名 説明
.eslintrc.js ESLintの設定ファイル
.gitignore Gitで管理しないファイルを指定するためのファイル
.npmrc npmの設定ファイル
README.md READMEファイル
package.json プロジェクトの設定ファイル
pnpm-lock.yaml pnpmでインストールしたパッケージの情報が記録されているファイル
pnpm-workspace.yaml pnpmの設定ファイル
turbo.json ESLintの設定ファイル

プロジェクト構成の制約

今回作成した公式のサンプルでは、便宜上、アプリケーションは apps 配下に、パッケージは packages 配下に配置されています。しかし、これらの構成は Turborepo を利用する上で必須の構成ではありません。極論、以下のように、apps の配下に uieslint-config-customtsconfig が配置されていても設定が正しければ正しく動作します。ただし、公式は appspackages の構成を推奨しています。

.
├── apps
│   ├── docs
│   ├── web
│   ├── eslint-config-custom
│   ├── tsconfig
│   └── ui
├── .eslintrc.js
├── package.json
├── pnpm-workspace.yaml
├── README.md
└── turbo.json

ワークスペース

ワークスペースについて解説します。ワークスペースとはアプリケーションやパッケージを管理するための単位です。

サンプルで作成される5つのワークスペース

Turborepo のモノレポは複数のワークスペースから構成されます。前記の通り、今回作成した公式サンプルには 5 つのワークスペース(docswebeslint-config-customtsconfigui)が含まれています。

以下がそれぞれのワークスペースの概要です。

ワークスペース名 パス 説明
web apps/web TypeScriptのNext.jsアプリ
docs apps/docs TypeScriptのNext.jsアプリ
ui packages/ui 共有のReactコンポーネント
eslint-config-custom packages/eslint-config-custom 共有のESLint設定
tsconfig packages/tsconfig 共有のTypeScriptの設定
.
├── apps
│   ├── docs
│   └── web
└── packages
    ├── eslint-config-custom
    ├── tsconfig
    └── ui

package.json

各ワークスペースは package.json を必ず含む必要があり、package.jsonname フィールドの値によってワークスペースの名称が決まります。

apps/web/package.json
{
  "name": "web",
}
apps/docs/package.json
{
  "name": "docs",
}
packages/eslint-config-custom/package.json
{
  "name": "eslint-config-custom",
}
packages/tsconfig/package.json
{
  "name": "tsconfig",
}
packages/ui/package.json
{
  "name": "ui",
}

ワークスペース間の依存

ワークスペースにはそれぞれ役割をもたせて、依存関係をもたせることができます。

  • docsuieslint-config-customtsconfig に依存しています。
  • webuieslint-config-customtsconfig に依存しています。
  • uieslint-config-customtsconfig に依存しています。
  • tsconfig は依存していません。
  • eslint-config-custom は依存していません。

他ワークスペースを依存しているかどうかは、package.json で確認できます。参考までに、webpackage.json を確認すると、uieslint-config-customtsconfig への依存が確認できます。

apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "ui": "workspace:*"
  },
  "devDependencies": {
    "eslint-config-custom": "workspace:*",
    "tsconfig": "workspace:*",
  }
}

ワークスペースに依存する場合は、workspace:* という形で記述します。ワークスペースの詳細はpnpmのページに記載されています。

https://pnpm.io/workspaces

ローカルで動作確認

ローカルで実行しどのような結果になるか確認します。

$ pnpm i && pnpm build && pnpm dev

【参考】実行ログ

実行ログ
Scope: all 6 workspace projects
Lockfile is up to date, resolution step is skipped
Already up to date
Done in 876ms

> my-turborepo@ build /Users/hayato94087/Private/my-turborepo
> turbo run build

• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running build in 5 packages
• Remote caching disabled
web:build: cache miss, executing a22776b494a1f7b2
docs:build: cache miss, executing 6836a2f4497ba1de
web:build: 
web:build: > web@1.0.0 build /Users/hayato94087/Private/my-turborepo/apps/web
web:build: > next build
web:build: 
docs:build: 
docs:build: > docs@1.0.0 build /Users/hayato94087/Private/my-turborepo/apps/docs
docs:build: > next build
docs:build: 
docs:build: info  - Linting and checking validity of types...
web:build: info  - Linting and checking validity of types...
docs:build: info  - Creating an optimized production build...
web:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
docs:build: info  - Compiled successfully
web:build: info  - Collecting page data...
docs:build: info  - Collecting page data...
docs:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (3/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
docs:build: info  - Finalizing page optimization...
web:build: 
docs:build: 
web:build: Route (pages)                              Size     First Load JS
web:build: ┌ ○ /                                      301 B          74.2 kB
web:build: └ ○ /404                                   182 B          74.1 kB
web:build: + First Load JS shared by all              73.9 kB
web:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
web:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
web:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
web:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
web:build: 
docs:build: Route (pages)                              Size     First Load JS
docs:build: ┌ ○ /                                      302 B          74.2 kB
docs:build: └ ○ /404                                   182 B          74.1 kB
docs:build: + First Load JS shared by all              73.9 kB
docs:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
docs:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
docs:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
docs:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
docs:build: 
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build: 
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build: 

 Tasks:    2 successful, 2 total
Cached:    0 cached, 2 total
  Time:    22.644s 


> my-turborepo@ dev /Users/hayato94087/Private/my-turborepo
> turbo run dev

• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running dev in 5 packages
• Remote caching disabled
web:dev: cache bypass, force executing 41ce06a2e6649cc6
docs:dev: cache bypass, force executing ef76b7b2ab004403
web:dev: 
web:dev: > web@1.0.0 dev /Users/hayato94087/Private/my-turborepo/apps/web
web:dev: > next dev
web:dev: 
docs:dev: 
docs:dev: > docs@1.0.0 dev /Users/hayato94087/Private/my-turborepo/apps/docs
docs:dev: > next dev --port 3001
docs:dev: 
docs:dev: ready - started server on 0.0.0.0:3001, url: http://localhost:3001
web:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
web:dev: event - compiled client and server successfully in 1271 ms (156 modules)
docs:dev: event - compiled client and server successfully in 1284 ms (156 modules)

webhttp://localhost:3000 が動作します。

docshttp://localhost:3001 が動作します。

uiを探索

ui は他のワークスペースに共有の React コンポーネントライブラリーを提供するためのワークスペースです。ここでは、ui の中身を探索していきます。

ワークスペースの中身

packages/ui のフォルダ構成は以下のとおりです。

フォルダ構成
.
└── packages
    └── ui
        ├── Button.tsx
        ├── index.tsx
        ├── package.json
        └── tsconfig.json
ファイル名 説明
Button.tsx 他ワークスペースに共有されるReactのコンポーネント
index.tsx Reactのコンポーネントをexportしているファイル
package.json 設定ファイル
tsconfig.json TypeScriptの設定ファイル

package.json

以下が packagejson です。

packages/ui/package.json
{
  "name": "ui",
  "version": "0.0.0",
  "main": "./index.tsx",
  "types": "./index.tsx",
  "license": "MIT",
  "scripts": {
    "lint": "eslint \"**/*.ts*\""
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "eslint": "^7.32.0",
    "eslint-config-custom": "workspace:*",
    "react": "^17.0.2",
    "tsconfig": "workspace:*",
    "typescript": "^4.5.2"
  }
}

ワークスペースの名前

あらためて、ワークスペースの名前は package.jsonname フィールドにて指定されています。

packages/ui/package.json
{
  "name": "ui",
}

他のワークスペースへの依存

uieslint-config-customtsconfig に依存しています。

packages/ui/package.json
{
  "devDependencies": {
    "eslint-config-custom": "workspace:*",
    "tsconfig": "workspace:*",
  }
}

eslint-config-customlint が実行可能です。

packages/ui/package.json
{
  "scripts": {
    "lint": "eslint \"**/*.ts*\""
  },
}

tsconfig は以下の設定ファイルで利用されています。

tsconfig.json
{
  "extends": "tsconfig/react-library.json",
  "include": ["."],
  "exclude": ["dist", "build", "node_modules"]
}

エクスポート

他のワークスペースが、ui ワークスペースをインポートする際に呼ばれるスクリプトファイルを指定している場所は package.jsonmain です。ui の場合は、index.tsx が呼び出されます。

packages/ui/package.json
{
  "name": "ui",
  "main": "./index.tsx",
  "types": "./index.tsx",
 }

index.tsxButton がエクスポートされています。これにより、ui に依存している webButton をインポートできます。

packages/ui/index.tsx
import * as React from "react";
export * from "./Button";
packages/ui/Button.tsx
import * as React from "react";

export const Button = () => {
  return <button>Boop</button>;
};

他ワークスペースからのインポート

web では、uiButton を利用しています。

apps/web/pages/index.tsx
import { Button } from "ui";

export default function Web() {
  return (
    <div>
      <h1>Web</h1>
      <Button />
    </div>
  );
}

下記の画面の通り、Boop と表示されています。

docsweb 同様に、uiButton をインポートし利用しています。説明は省略します。

コンポーネントの更新

文字列を Boop から Hello World に変更してみます。

packages/ui/Button.tsx
import * as React from "react";
export const Button = () => {
-  return <button>Boop</button>;
+  return <button>Hello World</button>;
};

ローカル環境で確認します。既に実行中であれば、ホットリローディングにて、変更はリアルタイムに反映されます。

$ pnpm dev

無事変更されました。

新しいコンポーネントを追加

新しくコンポーネントを ui に追加し、web で使ってみます。

コンポーネントを定義するファイルを作成します。

packages/ui/Button2.tsx
import * as React from "react";

export const Button2 = () => {
  return <button>Hello Universe</button>;
};

packages/ui/index.tsx に追加し Button2 を export します。

packages/ui/index.tsx
import * as React from "react";
export * from "./Button";
+export * from "./Button2";

apps/web/pages/index.tsxButton2 を追加します。

apps/web/pages/index.tsx
-import { Button } from "ui";
+import { Button, Button2 } from "ui";

export default function Web() {
  return (
    <div>
      <h1>Web</h1>
      <Button />
+      <Button2 />
    </div>
  );
}

ローカル環境で確認します。既に実行中であれば、ホットリローディングにて、変更はリアルタイムに反映されます。

コマンド
pnpm dev

無事追加されました。

tsconfigを探索

tsconfig は他のワークスペースへの共有の TypeScript の設定が保管されています。ここでは、tsconfig の中身を探索していきます。

ワークスペースの中身

packages/tsconfig のフォルダ構成は以下のとおりです。重要なもののみ記載しています。

.
└── packages
    └── tsconfig
        ├── base.json
        ├── nextjs.json
        ├── react-library.json
        └── package.json
ファイル名 説明
base.json TypeScriptの設定ファイル
nextjs.json base.jsonを拡張しているnext.js用のTypeScriptの設定ファイル
react-library.json base.jsonを拡張しているReactライブラリー用のTypeScriptの設定ファイル
package.json tsconfigワークスペースの設定ファイル

package.json

以下が packagejson です。

packages/tsconfig/package.json
{
  "name": "tsconfig",
  "version": "0.0.0",
  "private": true,
  "license": "MIT",
  "publishConfig": {
    "access": "public"
  }
}

ワークスペースの名前

あらためて、ワークスペースの名前は package.jsonname フィールドにて指定されています。

packages/tsconfig/package.json
{
  "name": "tsconfig",
}

他のワークスペースへの依存

tsconfig は他のワークスペースに依存していません。

エクスポート

package.jsonpublicConfig にて accesspublic に設定されています。これにより、tsconfig を他のワークスペースから利用できるようになります。

packages/tsconfig/package.json
{
  "publishConfig": {
    "access": "public"
  }
}

以下のように files を使用し特定ファイルを共有可能にできます。(以前のバージョンでは files が使用されていました。)

packages/tsconfig/package.json
{
  "files": [
    "base.json",
    "nextjs.json",
    "react-library.json"
  ]
}

インポート&拡張

tsconfig をインポートするワークスペースにて、tsconfig.json を作成します。作成した tsconfig.jsonextends を利用し tsconfig の TypeScript の設定をインポートします。設定は上書きも可能です。

uidocsweb では、tsconfig/react-library.json の設定を読み込み、includeexclude を利用し設定をカスタマイズしています。

packages/ui/tsconfig.json
{
  "extends": "tsconfig/react-library.json",
  "include": ["."],
  "exclude": ["dist", "build", "node_modules"]
}
apps/docs/tsconfig.json
{
  "extends": "tsconfig/react-library.json",
  "include": ["."],
  "exclude": ["node_modules"]
}
apps/web/tsconfig.json
{
  "extends": "tsconfig/nextjs.json",
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

eslint-config-customを探索

eslint-config-custom は他のワークスペースに共有の ESTLint の設定を提供するためのワークスペースです。ここでは、eslint-config-custom の中身を探索していきます。

ワークスペースの中身

packages/eslint-config-custom のフォルダ構成は以下のとおりです。重要なもののみ記載しています。

フォルダ構成
.
└── packages
    └── eslint-config-custom
        ├── index.js
        └── package.json
ファイル名 説明
index.js ESLintの設定ファイル
package.json eslint-config-customワークスペースの設定ファイル

ワークスペースの名前

あらためて、ワークスペースの名前は package.jsonname フィールドにて指定されています。

packages/eslint-config-custom/package.json
{
  "name": "eslint-config-custom",
}

ワークスペース名に、eslint-config- というプレフィックスが付いています。これは、ESLintの設定ファイルの命名規則に従っているためです。
https://eslint.org/

他のワークスペースへの依存

eslint-config-custom は他のワークスペースに依存していません。

エクスポート

package.jsonmain にて、index.js を指定しています。

packages/eslint-config-custom/package.json
{
  "name": "eslint-config-custom",
  "main": "index.js",
}

ESLintのルール

index.js には ESLint のルールが記述されています。

packages/eslint-config-custom/index.js
module.exports = {
  extends: ["next", "turbo", "prettier"],
  rules: {
    "@next/next/no-html-link-for-pages": "off",
  },
  parserOptions: {
    babelOptions: {
      presets: [require.resolve("next/babel")],
    },
  },
};

extends にて、nextturboprettier の設定を継承しています。詳細は補足参照。

【参考】eslint-config の補足

rules にて、@next/next/no-html-link-for-pages のルールを無効化しています。詳細は補足参照。

【参考】no-html-link-for-pages の補足

parserOptions にて、babelOptions を指定しています。詳細は補足参照。

【参考】parserOptions の補足


babelOptions は、ESLint が Babel という JavaScript のコンパイラを使って、コードを解析する際に適用されるオプションを指定します。Babel は、最新の JavaScript 機能を古いブラウザでも動作するように変換するために使用されます。presets オプションに require.resolve("next/babel")が指定されています。これは、Next.js が提供する Babel プリセットを適用することを意味します。プリセットは、Babel がどのようにコードを変換するかを決定する一連のプラグインです。Next.js の Babel プリセットは、Next.js アプリケーションに最適化された設定を含んでいます。まとめると、ESLint が Babel を使用してコードを解析する際に、Next.js の Babel プリセットを適用するように指定しています。これにより、ESLint が Next.js アプリケーションで適切な解析を行えるようになります。

インポート方法その1

2 つの方法で ESLint の設定をインポートできます。1 つ目の方法は eslint-config-custom をインポートするワークスペースにて、.eslintrc.js を作成します。extends にて eslint-config-custom の設定をインポート/拡張します。

webdocs は、eslint-config-custom に依存しています。

apps/web/package.json
{
  "name": "web",
  "devDependencies": {
    "eslint-config-custom": "workspace:*",
  }
}
apps/docs/package.json
{
  "name": "docs",
  "devDependencies": {
    "eslint-config-custom": "workspace:*",
  }
}

webdocs はワークスペースに、.eslintrc.js を配置しています。extends にて custom を指定することで、eslint-config-custom の設定をインポートしています。ESLint ルールを extends する際は、eslint-config- というプレフィックスを省略します。

apps/web/.eslintrc.js
module.exports = {
  root: true,
  extends: ["custom"],
};
apps/docs/.eslintrc.js
module.exports = {
  root: true,
  extends: ["custom"],
};

さらに、root: true を設定することで、実行時のカレントディレクトリを起点にして、上位のディレクトリの設定ファイル (.eslintrc.*) を探索しないように設定を拡張しています。

【参考】root: true の補足


ESLint の設定ファイル(.eslintrc.js)が各ワークスペースではなくルートに配置されています。ESLint は、実行時のカレントディレクトリを起点にして、上位のディレクトリの設定ファイル (.eslintrc.*) を探索していきます。ルートに配置することで、各ワークスペースの ESLint の設定ファイルを共通化できます。例えば、packages/ui には .eslintrc.js が存在しないため、次に packages を確認します。ここにも .eslintrc.js が存在しないため、次にルートを確認します。ルートに .eslintrc.js が存在するため、ルートの .eslintrc.js を利用します。他のワークスペースも同様な挙動をします。

インポート方法その2

2 つ目の方法は eslint-config-custom をインポートするワークスペースにて、.eslintrc.js を作成しない方法です。上の階層の .eslintrc.js を参照し eslint-config-custom の設定をインポートします。

ui は、eslint-config-custom に依存しています。

packages/ui/package.json
{
  "name": "ui",
  "devDependencies": {
    "eslint-config-custom": "workspace:*",
  }
}

ui はワークスペースに、.eslintrc.js を配置していません。ESLint は挙動として、実行時のカレントディレクトリを起点にして、上位のディレクトリの設定ファイル (.eslintrc.*) を探索していきます。この挙動を利用し、ルート(./)に配置されている .eslintrc.js を利用しています。

module.exports = {
  root: true,
  // This tells ESLint to load the config from the package `eslint-config-custom`
  extends: ["custom"],
  settings: {
    next: {
      rootDir: ["apps/*/"],
    },
  },
};

settings についての解説です。詳細はNext.jsの公式サイトに記述があります。
https://nextjs.org/docs/basic-features/eslint#custom-settings

ふりかえり

これまでのふりかえりです。

  • モノレポとは複数のパッケージを 1 つのリポジトリで管理するです
  • ワークスペースとはモノレポを構成する単位であり、モノレポは複数のワークスペースから構成されます。
  • ワークスペース間の依存関係を uitsconfigeslint-config-custom のワークスペースの探索を通し理解しました。

次は、turbo コマンドの詳細を理解していきます。

turbo.jsonを探索

turbo がどのようにタスクを実行するか学んでいきます。

turbo は、turbo.json というファイルを読み込み、タスクを実行します。turbo.json の内容は以下です。

turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

$schema,globalDependencies,pipeline について簡単に説明します。

$schema

$schema は、turbo.json のバリデーションを行うためのものです。https://turbo.build/schema.json にアクセスすると、JSON Schema が確認できます。

turbo.json
{
  "$schema": "https://turbo.build/schema.json",
}

https://turbo.build/schema.json

globalDependencies

globalDependencies は、ワークスペース間で共有するファイルを指定します。.env.*local というファイルを共有していますが、今回はファイルを作成していません。

turbo.json
{
  "globalDependencies": ["**/.env.*local"],
}

https://turbo.build/repo/docs/reference/configuration#globaldependencies

pipeline

実行するタスクは、turbo.jsonpipeline にて定義します。今回は pipeline にて、build, lint, dev のタスクが定義されています。

turbo.json
{
  "pipeline": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

以下の通り、タスクを指定し、turbo で実行することで、タスクを実行できます。以下は build のタスクを実行しています。

$ turbo run build

ちなみに、run を省略できます。

$ turbo build

また、タスクを複数指定できます。以下は buildlint のタスクを実行しています。

$ turbo run build lint

run を省略できます。

$ turbo build lint

続いて、実際にタスクを実行しながら turbo を理解していきます。

https://turbo.build/repo/docs/reference/configuration#pipeline

プロジェクトの再作成

Turborepo でタスクを実行するとローカルのファイルシステムにキャッシュが自動的に保存されます。キャッシュは、node_modules/.cache/turbo に保存されます。node_modules/.cache/turbo を削除することで、ローカルキャッシュを削除できます。キャッシュについて理解するために、念の為、プロジェクトを my-turborepo-2 で再作成します。

$ pnpm dlx create-turbo@latest

【参考】実行ログ

実行ログ
>>> TURBOREPO

>>> Welcome to Turborepo! Let's get you set up with a new codebase.

? Where would you like to create your turborepo? my-turborepo-2/
? Which package manager do you want to use? pnpm

Downloading files. This might take a moment.

>>> Created a new Turborepo with the following:

apps
 - apps/docs
 - apps/web
packages
 - packages/eslint-config-custom
 - packages/tsconfig
 - packages/ui

Installing packages. This might take a couple of minutes.

>>> Success! Created a new Turborepo at "my-turborepo-2".
Inside that directory, you can run several commands:

  pnpm run build
     Build all apps and packages

  pnpm run dev
     Develop all apps and packages

  pnpm run lint
     Lint all apps and packages

Turborepo will cache locally by default. For an additional
speed boost, enable Remote Caching with Vercel by
entering the following command:

  pnpm dlx turbo login

We suggest that you begin by typing:

  cd my-turborepo-2
  pnpm dlx turbo login

プロジェクト全体をLint

プロジェクト全体に対して Lint を実行します。

$ turbo run lint
実行ログ
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running lint in 5 packages
• Remote caching disabled
ui:lint: cache miss, executing 092988374bf8f828
docs:lint: cache miss, executing 188608743c4ff1e4
web:lint: cache miss, executing 997cb3ca5a2129cf
ui:lint: 
ui:lint: > ui@0.0.0 lint /Users/hayato94087/Private/my-turborepo-2/packages/ui
ui:lint: > eslint "**/*.ts*"
ui:lint: 
docs:lint: 
docs:lint: > docs@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:lint: > next lint
docs:lint: 
web:lint: 
web:lint: > web@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/web
web:lint: > next lint
web:lint: 
web:lint: ✔ No ESLint warnings or errors
docs:lint: ✔ No ESLint warnings or errors

 Tasks:    3 successful, 3 total
Cached:    0 cached, 3 total
  Time:    3.88s 

次のセクションで Cache について解説するので、実行ログの最後のアウトプットについて覚えておいてください。

実行ログの抜粋
 Tasks:    3 successful, 3 total
Cached:    0 cached, 3 total
  Time:    3.88s 

turbo run lint の裏では、5 つのワークスペースに対して lint のタスクが実行されます。webdocsuilint が定義されているので lint が実行されます。

apps/web/package.json
{
  "name": "web",
  "scripts": {
    "lint": "next lint"
  },
}
apps/docs/package.json
{
  "name": "docs",
  "scripts": {
    "lint": "next lint"
  },
}
packages/docs/package.json
{
  "name": "ui",
  "scripts": {
    "lint": "eslint \"**/*.ts*\""
  },
}

eslint-config-customtsconfig、は package.jsonlint が定義されていないため、lint は実行されません。

ローカルキャッシュについて

Turborepo は実行結果をキャッシュしておくことで、変更がない場合は実行をスキップします。これにより、実行時間を短縮できます。

あらためて、もう一度、Lint を実行します。

$ turbo run lint

実行ログです。

実行ログ
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running lint in 5 packages
• Remote caching disabled
ui:lint: Skipping cache check for ui#lint, outputs have not changed since previous run.
ui:lint: cache hit, replaying output 092988374bf8f828
docs:lint: Skipping cache check for docs#lint, outputs have not changed since previous run.
docs:lint: cache hit, replaying output 188608743c4ff1e4
ui:lint: 
ui:lint: > ui@0.0.0 lint /Users/hayato94087/Private/my-turborepo-2/packages/ui
ui:lint: > eslint "**/*.ts*"
ui:lint: 
docs:lint: 
docs:lint: > docs@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:lint: > next lint
docs:lint: 
docs:lint: ✔ No ESLint warnings or errors
web:lint: cache hit, replaying output 997cb3ca5a2129cf
web:lint: 
web:lint: > web@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/web
web:lint: > next lint
web:lint: 
web:lint: ✔ No ESLint warnings or errors

 Tasks:    3 successful, 3 total
Cached:    3 cached, 3 total
  Time:    414ms >>> FULL TURBO

上記のログから、キャッシュが有効になっているログを以下に抜粋しています。docs,web,ui とキャッシュが有効になっていることがわかります。

実行ログの抜粋
ui:lint: cache hit, replaying output 092988374bf8f828
docs:lint: cache hit, replaying output 188608743c4ff1e4
web:lint: cache hit, replaying output 997cb3ca5a2129cf

また、初回に、turbo lint を実行した際に、最後の結果は以下でした。

実行ログの抜粋
 Tasks:    3 successful, 3 total
Cached:    0 cached, 3 total
  Time:    3.88s 

2 回目は、以下です。

実行ログの抜粋
 Tasks:    3 successful, 3 total
Cached:    3 cached, 3 total
  Time:    414ms >>> FULL TURBO

1 回目は、0 cached とあるように、キャッシュが 1 件も効きませんでした。

2 回目は、3 cached とあり、cache hit のメッセージからも、docs,web,ui の 3 件のキャッシュが効きました。実行時間も 3.88s から 414ms に短縮されました。このようにキャッシュにより、変更がないタスクの処理については短縮化されます。

試しに、web のページを更新し、Lint を実行します。

apps/web/pages/index.tsx
import { Button } from "ui";

export default function Web() {
  return (
    <div>
-      <h1>Web</h1>
+      <h1>Web2</h1>
      <Button />
    </div>
  );
}
$ turbo run lint
実行ログ
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running lint in 5 packages
• Remote caching disabled
ui:lint: Skipping cache check for ui#lint, outputs have not changed since previous run.
ui:lint: cache hit, replaying output 092988374bf8f828
docs:lint: Skipping cache check for docs#lint, outputs have not changed since previous run.
docs:lint: cache hit, replaying output 188608743c4ff1e4
web:lint: cache miss, executing fa9cd72ef8223180
docs:lint: 
docs:lint: > docs@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:lint: > next lint
ui:lint: 
ui:lint: > ui@0.0.0 lint /Users/hayato94087/Private/my-turborepo-2/packages/ui
ui:lint: > eslint "**/*.ts*"
ui:lint: 
docs:lint: 
docs:lint: ✔ No ESLint warnings or errors
web:lint: 
web:lint: > web@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/web
web:lint: > next lint
web:lint: 
web:lint: ✔ No ESLint warnings or errors

 Tasks:    3 successful, 3 total
Cached:    2 cached, 3 total
  Time:    3.37s 

期待したとおり、docs,ui の 2 件にはキャッシュが効いていますが、変更しているため cache miss と出ているように web にはキャッシュが効きませんでした。

実行ログの抜粋
ui:lint: cache hit, replaying output 092988374bf8f828
docs:lint: cache hit, replaying output 188608743c4ff1e4
web:lint: cache miss, executing fa9cd72ef8223180
実行ログの抜粋
 Tasks:    3 successful, 3 total
Cached:    2 cached, 3 total
  Time:    3.37s 

存在しないタスクの実行

ちなみに tubro.json に存在しないタスクを実行した場合どうなるかも見ておきます。試しに、存在しないタスク hello を実行します。

$ turbo run hello

以下の通りタスクが見つからないとエラーが出ました。

実行ログ
ERROR  run failed: error preparing engine: Could not find the following tasks in project: hello
Turbo error: error preparing engine: Could not find the following tasks in project: hello

turbo.json で定義されていないタスクが実行できません。

プロジェクト全体をビルド

続いて、プロジェクト全体をビルドするために、turbobuild のタスクを実行します。

$ turbo run build

以下が実行ログです。build は 1 度も実行していないため、cache miss と出ているように、キャッシュにはヒットしていません。

実行ログ
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running build in 5 packages
• Remote caching disabled
web:build: cache miss, executing f60525cfaa657509
docs:build: cache miss, executing 6836a2f4497ba1de
web:build: 
web:build: > web@1.0.0 build /Users/hayato94087/Private/my-turborepo-2/apps/web
web:build: > next build
web:build: 
docs:build: 
docs:build: > docs@1.0.0 build /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:build: > next build
docs:build: 
docs:build: info  - Linting and checking validity of types...
web:build: info  - Linting and checking validity of types...
web:build: info  - Creating an optimized production build...
docs:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
docs:build: info  - Compiled successfully
web:build: info  - Collecting page data...
docs:build: info  - Collecting page data...
web:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
web:build: 
web:build: Route (pages)                              Size     First Load JS
web:build: ┌ ○ /                                      302 B          74.2 kB
web:build: └ ○ /404                                   182 B          74.1 kB
web:build: + First Load JS shared by all              73.9 kB
web:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
web:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
web:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
web:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
web:build: 
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build: 
docs:build: info  - Generating static pages (3/3)
docs:build: info  - Finalizing page optimization...
docs:build: 
docs:build: Route (pages)                              Size     First Load JS
docs:build: ┌ ○ /                                      302 B          74.2 kB
docs:build: └ ○ /404                                   182 B          74.1 kB
docs:build: + First Load JS shared by all              73.9 kB
docs:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
docs:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
docs:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
docs:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
docs:build: 
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build: 

 Tasks:    2 successful, 2 total
Cached:    0 cached, 2 total
  Time:    21.327s 

turbo run build の裏では、5 つのワークスペースに対して build のタスクが実行されます。webdocsbuild が定義されているので build が実行されます。

apps/web/package.json
{
  "name": "web",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
}
apps/docs/package.json
{
  "name": "docs",
  "scripts": {
    "dev": "next dev --port 3001",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
}

eslint-config-customtsconfiguibuild が定義されていないため、build は実行されません。

  • eslint-config-customtsconfigpackage.jsonscripts がありません。
  • ui には package.jsonscripts はありますが、build は定義されています。

もう一度、build してみます。

$ turbo run build

以下が実行ログです。

実行ログ
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running build in 5 packages
• Remote caching disabled
web:build: Skipping cache check for web#build, outputs have not changed since previous run.
web:build: cache hit, replaying output f60525cfaa657509
docs:build: Skipping cache check for docs#build, outputs have not changed since previous run.
docs:build: cache hit, replaying output 6836a2f4497ba1de
web:build: 
docs:build: 
docs:build: > docs@1.0.0 build /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:build: > next build
docs:build: 
docs:build: info  - Linting and checking validity of types...
web:build: > web@1.0.0 build /Users/hayato94087/Private/my-turborepo-2/apps/web
web:build: > next build
web:build: 
web:build: info  - Linting and checking validity of types...
web:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
web:build: info  - Collecting page data...
web:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
web:build: 
web:build: Route (pages)                              Size     First Load JS
web:build: ┌ ○ /                                      302 B          74.2 kB
web:build: └ ○ /404                                   182 B          74.1 kB
web:build: + First Load JS shared by all              73.9 kB
web:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
docs:build: info  - Creating an optimized production build...
docs:build: info  - Compiled successfully
docs:build: info  - Collecting page data...
docs:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (3/3)
docs:build: info  - Finalizing page optimization...
docs:build: 
docs:build: Route (pages)                              Size     First Load JS
docs:build: ┌ ○ /                                      302 B          74.2 kB
docs:build: └ ○ /404                                   182 B          74.1 kB
docs:build: + First Load JS shared by all              73.9 kB
docs:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
docs:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
docs:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
docs:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
docs:build: 
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build: 
web:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
web:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
web:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
web:build: 
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build: 

 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    422ms >>> FULL TURBO

build は 1 度実行しているため、cache hit と出ているように、キャッシュにヒットしています。以下がログの抜粋です。

実行ログの抜粋
web:build: cache hit, replaying output f60525cfaa657509
docs:build: cache hit, replaying output 6836a2f4497ba1de
実行ログの抜粋
 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    422ms >>> FULL TURBO

outputsについて

turbo.json を確認すると、outputsbuild には記述されています。webdocs は自身のワークスペースをビルとすると、それぞれのワークスペース配下のフォルダ(./.next)に実行結果が保存されます。outputs で、それらフォルダを指定することで、タスク実行後に、指定されたディレクトリの中身を Turborepo に記憶させることができます。記憶させることで、指定のディレクトリを削除してもプロジェクトに変更なければキャッシュから復元できます。

turbo.json
{
  "pipeline": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"]
    },
  }
}

web./.next のフォルダを削除して、もう一度ビルドしてみます。

$ rm -rf apps/web/.next && turbo build
実行ログ
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running build in 5 packages
• Remote caching disabled
docs:build: Skipping cache check for docs#build, outputs have not changed since previous run.
docs:build: cache hit, replaying output 6836a2f4497ba1de
docs:build: 
docs:build: > docs@1.0.0 build /Users/hayato94087/Private/my-turborepo/apps/docs
docs:build: > next build
docs:build: 
docs:build: info  - Linting and checking validity of types...
docs:build: info  - Creating an optimized production build...
docs:build: info  - Compiled successfully
docs:build: info  - Collecting page data...
docs:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (3/3)
docs:build: info  - Finalizing page optimization...
docs:build: 
docs:build: Route (pages)                              Size     First Load JS
docs:build: ┌ ○ /                                      302 B          74.2 kB
docs:build: └ ○ /404                                   182 B          74.1 kB
docs:build: + First Load JS shared by all              73.9 kB
docs:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
docs:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
docs:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
docs:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
docs:build: 
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build: 
web:build: cache hit, replaying output 14f1c96235930a1d
web:build: 
web:build: > web@1.0.0 build /Users/hayato94087/Private/my-turborepo/apps/web
web:build: > next build
web:build: 
web:build: info  - Linting and checking validity of types...
web:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
web:build: info  - Collecting page data...
web:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
web:build: 
web:build: Route (pages)                              Size     First Load JS
web:build: ┌ ○ /                                      302 B          74.2 kB
web:build: └ ○ /404                                   182 B          74.1 kB
web:build: + First Load JS shared by all              73.9 kB
web:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
web:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
web:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
web:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
web:build: 
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build: 

 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    651ms >>> FULL TURBO

一瞬でビルドが終わり、削除したディレクトリも復元されました。以下はログの抜粋です。

実行ログの抜粋
docs:build: cache hit, replaying output 6836a2f4497ba1de
web:build: cache hit, replaying output 14f1c96235930a1d
実行ログの抜粋
 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    651ms >>> FULL TURBO

理解が正しければ、FULL TURBO と出ている場合、キャッシュのミスが無いことを指します。

プロジェクト全体を開発環境で起動

今度は turbo run dev を実行します。lintdev 同様に、turbo dev の裏では、5 つのワークスペースに対して dev のタスクが実行されます。webdocsdev が定義されているので dev が実行されます。eslint-config-customtsconfiguidev が定義されていないため、dev は実行されません。

以下が dev で実際に実行されるコマンドです。docs--port 3001 で実行するポート番号を指定しています。

apps/docs/package.json
{
  "name": "docs",
  "scripts": {
    "dev": "next dev --port 3001",
  },
}
apps/web/package.json
{
  "name": "web",
  "scripts": {
    "dev": "next dev",
  },
}
$ turbo run dev

以下が実行ログです。

実行ログ
• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running dev in 5 packages
• Remote caching disabled
docs:dev: cache bypass, force executing ef76b7b2ab004403
web:dev: cache bypass, force executing 29abc4a5fc01acc6
web:dev: 
web:dev: > web@1.0.0 dev /Users/hayato94087/Private/my-turborepo-2/apps/web
web:dev: > next dev
web:dev: 
docs:dev: 
docs:dev: > docs@1.0.0 dev /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:dev: > next dev --port 3001
docs:dev: 
web:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
docs:dev: ready - started server on 0.0.0.0:3001, url: http://localhost:3001
docs:dev: event - compiled client and server successfully in 1550 ms (156 modules)
web:dev: event - compiled client and server successfully in 1547 ms (156 modules)

実行ログから、webhttp://localhost:3000 で、docshttp://localhost:3001 で実行されている事がわかります。また、実行ログで、cache bypass, force executing. と出ています。

実行ログの抜粋
docs:dev: cache bypass, force executing ef76b7b2ab004403
web:dev: cache bypass, force executing 29abc4a5fc01acc6
実行ログの抜粋
web:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
docs:dev: ready - started server on 0.0.0.0:3001, url: http://localhost:3001

実行ログから、キャッシュヒットが発生していないことがわかります。

turbo.json を確認します。

{
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

cachefalse になっています。つまり何もキャッシュしないように設定されています。これは、開発環境を実行しているだけで、何もキャッシュすべきコンテンツを生成しないため、キャッシュを無効にしています。詳細は以下を参照ください。

https://turbo.build/repo/docs/core-concepts/caching#turn-off-caching

また、"persistent": true は長期実行されるタスクが、他のタスクによって中断されないようにするための設定です。dev は長期実行されるタスクなので、"persistent": true が設定されています。"persistent": true については、公式サイトの説明がわかりにくいです。

https://turbo.build/repo/docs/reference/configuration#persistent

特定のワークスペースに対してのみタスクを実行

デフォルトでは、turbo run dev とすると、全てのワークスペースの dev が実行されます。しかし、--filter={ワークスペース名} でワークスペースを絞って実行できます。以下では、web のみ開発環境を実行しています。

turbo dev --filter=web
実行ログ
• Packages in scope: web
• Running dev in 1 packages
• Remote caching disabled
web:dev: cache bypass, force executing 29abc4a5fc01acc6
web:dev: 
web:dev: > web@1.0.0 dev /Users/hayato94087/Private/my-turborepo-2/apps/web
web:dev: > next dev
web:dev: 
web:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
web:dev: event - compiled client and server successfully in 505 ms (156 modules)

実行ログから、web のみが実行されていることがわかります。

--filter については、以下を参照ください。

https://turbo.build/repo/docs/core-concepts/monorepos/filtering

pnpmを利用しturboコマンドを実行

ルートからプロジェクト管理する場合、turbo を直接使うのではなく、pnpm を使って turbo を実行します。以下が、package.jsonscript です。

package.json
{
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
}

script の中で turbo が指定されていることがわかります。lint を実行する場合は、以下のように実行します。

$ pnpm lint
実行ログ
> my-turborepo-2@ lint /Users/hayato94087/Private/my-turborepo-2
> turbo run lint

• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running lint in 5 packages
• Remote caching disabled
ui:lint: Skipping cache check for ui#lint, outputs have not changed since previous run.
ui:lint: cache hit, replaying output 092988374bf8f828
docs:lint: Skipping cache check for docs#lint, outputs have not changed since previous run.
docs:lint: cache hit, replaying output 188608743c4ff1e4
docs:lint: 
docs:lint: > docs@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:lint: > next lint
docs:lint: 
ui:lint: 
ui:lint: > ui@0.0.0 lint /Users/hayato94087/Private/my-turborepo-2/packages/ui
ui:lint: > eslint "**/*.ts*"
ui:lint: 
docs:lint: ✔ No ESLint warnings or errors
web:lint: cache hit, replaying output fa9cd72ef8223180
web:lint: 
web:lint: > web@1.0.0 lint /Users/hayato94087/Private/my-turborepo-2/apps/web
web:lint: > next lint
web:lint: 
web:lint: ✔ No ESLint warnings or errors

 Tasks:    3 successful, 3 total
Cached:    3 cached, 3 total
  Time:    461ms >>> FULL TURBO

build を実行する場合は、以下のように実行します。

$ pnpm build
実行ログ
> my-turborepo-2@ build /Users/hayato94087/Private/my-turborepo-2
> turbo run build

• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running build in 5 packages
• Remote caching disabled
docs:build: cache hit, replaying output 6836a2f4497ba1de
web:build: cache hit, replaying output f60525cfaa657509
docs:build: 
web:build: 
web:build: > web@1.0.0 build /Users/hayato94087/Private/my-turborepo-2/apps/web
web:build: > next build
web:build: 
web:build: info  - Linting and checking validity of types...
docs:build: > docs@1.0.0 build /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:build: > next build
docs:build: 
docs:build: info  - Linting and checking validity of types...
docs:build: info  - Creating an optimized production build...
web:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
web:build: info  - Collecting page data...
web:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
web:build: 
web:build: Route (pages)                              Size     First Load JS
web:build: ┌ ○ /                                      302 B          74.2 kB
web:build: └ ○ /404                                   182 B          74.1 kB
web:build: + First Load JS shared by all              73.9 kB
web:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
web:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
web:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
web:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
web:build: 
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build: 
docs:build: info  - Compiled successfully
docs:build: info  - Collecting page data...
docs:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (3/3)
docs:build: info  - Finalizing page optimization...
docs:build: 
docs:build: Route (pages)                              Size     First Load JS
docs:build: ┌ ○ /                                      302 B          74.2 kB
docs:build: └ ○ /404                                   182 B          74.1 kB
docs:build: + First Load JS shared by all              73.9 kB
docs:build:   ├ chunks/framework-ffffd4e8198d9762.js   45.2 kB
docs:build:   ├ chunks/main-c781174d1546c2ca.js        27.8 kB
docs:build:   ├ chunks/pages/_app-7b4ea0a6077fc727.js  195 B
docs:build:   └ chunks/webpack-4e7214a60fad8e88.js     712 B
docs:build: 
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build: 

 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    446ms >>> FULL TURBO

dev を実行する場合は、以下のように実行します。

$ pnpm dev
実行ログ
> my-turborepo-2@ dev /Users/hayato94087/Private/my-turborepo-2
> turbo run dev

• Packages in scope: docs, eslint-config-custom, tsconfig, ui, web
• Running dev in 5 packages
• Remote caching disabled
docs:dev: cache bypass, force executing ef76b7b2ab004403
web:dev: cache bypass, force executing 29abc4a5fc01acc6
web:dev: 
web:dev: > web@1.0.0 dev /Users/hayato94087/Private/my-turborepo-2/apps/web
web:dev: > next dev
web:dev: 
docs:dev: 
docs:dev: > docs@1.0.0 dev /Users/hayato94087/Private/my-turborepo-2/apps/docs
docs:dev: > next dev --port 3001
docs:dev: 
docs:dev: ready - started server on 0.0.0.0:3001, url: http://localhost:3001
web:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
web:dev: event - compiled client and server successfully in 564 ms (156 modules)
docs:dev: event - compiled client and server successfully in 581 ms (156 modules)

--filter を利用する場合は以下のように実行します。

$ pnpm dev --filter=web
実行ログ
> my-turborepo-2@ dev /Users/hayato94087/Private/my-turborepo-2
> turbo run dev "--filter=web"

• Packages in scope: web
• Running dev in 1 packages
• Remote caching disabled
web:dev: cache bypass, force executing 29abc4a5fc01acc6
web:dev: 
web:dev: > web@1.0.0 dev /Users/hayato94087/Private/my-turborepo-2/apps/web
web:dev: > next dev
web:dev: 
web:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
web:dev: event - compiled client and server successfully in 583 ms (156 modules)

パッケージをインストールする場合

特定のワークスペースにパッケージをインストールしたい場合があります。以下のように --filter を利用することで、フォルダ移動することなくルート(./)からインストールできます。

以下では react-icons パッケージをインストールします。

pnpm i react-icons --filter=web
apps/web/package.json
{
  "name": "web",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "latest",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
+    "react-icons": "^4.8.0",
    "ui": "workspace:*"
  },
  "devDependencies": {
    "@types/node": "^17.0.12",
    "@types/react": "^18.0.22",
    "@types/react-dom": "^18.0.7",
    "eslint-config-custom": "workspace:*",
    "tsconfig": "workspace:*",
    "typescript": "^4.5.3"
  }
}

react-icons を追加します。

apps/web/pages/index.tsx
import { Button } from "ui";
+import { FaBeer } from "react-icons/fa";

export default function Web() {
  return (
    <div>
      <h1>Web2</h1>
      <Button />
+      <div>
+        <FaBeer />
+      </div>
    </div>
  );
}

実行します。

$ pnpm dev --filter=web   

アイコンが追加されました。

ローカルキャッシュの削除

ローカルキャッシュは ./node_modules/.cache/turbo/ に保存されると記載されています。

https://turbo.build/repo/docs/core-concepts/caching

が、こちらの内容を削除しても、残ることがあるため、./node_modules/.cache/turbo/ 以外の場所にも保存されていると思われます。

リモートキャッシュについて

最後に、リモートキャッシュについて解説します。

Turborepo のリモートキャッシュ機能は、ビルドやテストの結果をリモートサーバーに保存して、チーム全体で共有できる機能です。これにより、ビルド時間の短縮や効率的なリソース利用が実現できます。リモートキャッシュは、特に大規模なプロジェクトや分散したチームでの開発において有用です。

https://turbo.build/repo/docs/core-concepts/remote-caching

まとめ

本記事では、以下について解説しました。

  • モノレポとマルチレポの違い
  • Turborepo の概要
  • Turborepo のチュートリアル

知識の棚卸しのために書き始めたら、1 ヶ月ぐらいまとめるの時間かかりました。自身のプロジェクトでは少数のメンバーで開発しており、かつ、技術スタックは Next.js TypeScript を採用しています。Turborepo を採用することで、開発効率が向上できていると考えています。

Discussion