最新のTypeScriptモノレポを構築する

Table of Contents
モノレポの進化
モノレポは、JavaScriptおよびTypeScriptコミュニティにおいて、歴史的に賛否両論がありました。長い間、エンタープライズグレードのアプリケーションの規模、複雑さ、パフォーマンス要件に対応できるツールが単純に不足していました。Lernaのようなツールは道を開きましたが、多くの場合、開発者は遅いビルド、厄介なパッケージの巻き上げ(hoisting)問題、そして広範な採用を妨げる急な学習曲線に苦しめられました。
しかし、今日では状況は完全に変わりました。pnpmワークスペースとTurborepoの登場は、現代のTypeScriptコードベースの構造化と保守の方法に革命をもたらしました。もはや、数十のアプリケーションと共有パッケージを単一のGitリポジトリに収めることが可能であるだけでなく、複数のチームにわたるエンジニアリング作業をスケールさせる最も効率的な方法であると言えるでしょう。
このガイドでは、最新のTypeScriptモノレポの構築について深く掘り下げていきます。「Hello World」のような基本的な例はスキップし、堅牢で本番環境に対応したアーキテクチャに必要な高度なテクニックを探求します。このガイドは、大規模なモノリシックリポジトリをデプロイする際に遭遇する現実世界の課題に対処するために、基礎を超えた内容を提供します。
なぜpnpmワークスペースなのか?
Turborepoに飛び込む前に、堅牢なパッケージマネージャーが必要です。npmとYarnはどちらもワークスペースをサポートしていますが、pnpmは、その独自の依存関係解決とディスクスペース管理のアプローチにより、モノレポにとって明確な勝者として浮上しました。
すべての依存関係をルートのnode_modulesフォルダに巻き上げる(依存関係ツリーをフラット化する)従来のパッケージマネージャーとは異なり、pnpmはコンテンツアドレス指定可能なストアとシンボリックリンクを使用します。これにより、パッケージがそのpackage.jsonに明示的にリストされている依存関係のみにアクセスできる厳格なnode_modules構造が実現します。
この厳格さはモノレポにおいて非常に重要です。これは「ファントム依存関係(phantom dependencies)」を防ぎます。これは、パッケージAがパッケージBによってインストールされた推移的依存関係に誤って依存してしまうという悪名高い問題です。パッケージBが削除または更新されると、パッケージAが予期せず壊れ、多くの場合、継続的インテグレーション(CI)またはデプロイ時にのみ発見されます。pnpmを使用すると、ファントム依存関係は事実上排除され、すべてのワークスペースで決定論的で信頼性の高いビルドが保証されます。
pnpmワークスペースを有効にするには、リポジトリのルートにpnpm-workspace.yamlファイルを作成するだけです。
packages:
- 'apps/*'
- 'packages/*'
Turborepoの登場:高性能ビルドシステム
pnpmが依存関係を管理する一方で、モノレポ全体でタスクを調整するためのビルドシステムが必要です。ここでTurborepoが輝きます。Turborepoは、Go(および新しいイテレーションではRust)で書かれた高性能ビルドシステムであり、インテリジェントなキャッシュと並列実行を利用してビルド時間を劇的に短縮します。
Turborepoはワークスペースの依存関係グラフを理解しています。もしapp-webがpackages/uiとpackages/utilsに依存している場合、Turborepoはapp-webをビルドする前にuiとutilsをビルドする必要があることを知っています。
さらに重要なのは、これらのタスクの出力をキャッシュすることです。もしpackages/utilsを変更していない場合、TurborepoはCPUサイクルを無駄にして再ビルドすることはありません。以前の実行からのキャッシュされた出力を即座に再生し、タスク全体をスキップします。
turbo.jsonの設定
Turborepoの心臓部はturbo.json設定ファイルです。以下は、高度で本番環境に対応したセットアップの例です。
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}
^build構文に注目してください。これはTurborepoに対し、特定のパッケージのbuildタスクが、その依存関係のbuildタスクに依存していることを伝えます。このシンプルな構文により、トポロジカルソートが可能になり、依存関係グラフに基づいてすべてが正しい数学的順序でビルドされることが保証されます。
高度なTypeScript設定
TypeScriptモノレポで最も難しい部分の1つは、tsconfig.jsonファイルを正しく設定することです。共通の設定を共有して乖離を防ぎつつ、特定のパッケージが設定を上書きできるようにしたいと考えています(例:ReactアプリはNode.js CLIツールとは異なるコンパイラオプションが必要です)。
ベース設定戦略
基盤となる設定をエクスポートする中央のtsconfigパッケージ(例:packages/tsconfig)を作成します。これにより、重複を防ぎ、一貫性を強制します。
// packages/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true
},
"exclude": ["node_modules"]
}
その後、react-library.jsonやnextjs.jsonのようなバリエーションを提供できます。実際のアプリやパッケージでは、これらのベース設定を拡張し、ローカルファイルをクリーンに保ち、上書きに集中させます。
// apps/web/tsconfig.json
{
"extends": "@my-org/tsconfig/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
統一されたリンティングとフォーマット戦略
複数のプロジェクト間で一貫したコードスタイルを維持することは、適切に設計されたモノレポの証です。すべてのアプリケーションとパッケージでバラバラの.eslintrc.jsファイルと.prettierrcファイルを維持するのではなく、TypeScriptの設定と同様に、これらの設定を一元化できます。
packages/eslint-configディレクトリを作成します。その中に、eslint-config-turboやeslint-config-prettierのような人気のある設定を拡張して、標準ルールを定義します。これらのルールをパッケージとしてエクスポートすることで、他のワークスペースはローカル設定でそれらを拡張するだけで済みます。この単一の真実源(single-source-of-truth)モデルは、設定の乖離を防ぎ、あるパッケージで適用されたリンティングの修正がリポジトリ全体で強制されることを保証します。さらに、ルートのturbo.jsonにlintスクリプトを定義することで、すべてのワークスペースでリンティングタスクを並行して実行でき、継続的インテグレーション中のフィードバックループを劇的に短縮できます。
内部パッケージとコード共有
現代のモノレポは、内部パッケージに大きく依存しています。複数のアプリケーションでユーティリティ関数やUIコンポーネントを重複させる代わりに、それらをpackages/utilsやpackages/uiのような専用のワークスペースに抽出します。
pnpmワークスペースでは、workspace:*プロトコルを使用してこれらを内部的にリンクします。apps/web/package.jsonでは次のようになります。
{
"dependencies": {
"@my-org/ui": "workspace:*",
"@my-org/utils": "workspace:*"
}
}
workspace:*プロトコルは、常にパッケージのローカルバージョンを使用することを保証します。Turborepoと組み合わせることで、@my-org/uiの変更は必要に応じてapps/webの再ビルドを自動的にトリガーし、すべてキャッシュを尊重します。これにより、パッケージ間の変更が即座に反映されるシームレスな開発者エクスペリエンスが生まれます。
ビルドするか、しないか?
これらの内部パッケージを設定する際の一般的なアーキテクチャ上の決定は、それらを事前にコンパイル(dist/ディレクトリにビルド)するか、TypeScriptソースコードとして直接コンシューマアプリケーションにインポートするかです。
アプローチ1:事前コンパイルされたパッケージ
これは最も安全で伝統的な方法です。各パッケージは、tsc、tsup、またはviteのようなバンドラーまたはコンパイラを使用して、それ自体をビルドする責任があります。その後、コンシューマはトランスパイルされた.jsファイルと.d.tsファイルをインポートします。これにより、厳密な境界が強制され、互換性が保証されますが、すべての内部パッケージに専用のビルドステップが必要です。
アプローチ2:ソースインポート(ジャストインタイムコンパイル)
この現代的なアプローチでは、Next.jsやVite(apps/ディレクトリ内)のようなアプリケーションフレームワークが、内部パッケージのTypeScriptソースコードを直接トランスパイルするように設定されます。これにより、内部パッケージのビルドステップが完全に不要になり、中間ビルドプロセスを待つ必要がないため、ローカル開発が劇的に高速化されます。next-transpile-modules(Next.jsバージョン13以降に直接組み込まれています)のようなツールは、このワークフローをシームレスにします。
ソースインポートはローカル開発では非常に高速ですが、事前コンパイルされたパッケージはより強力なカプセル化を提供し、パッケージをnpmのようなレジストリに外部公開する予定がある場合は厳密に必要です。ハイブリッドアプローチが一般的にベストプラクティスとされています。内部専用の共有コードにはソースインポートを使用し、公開を意図したライブラリは事前コンパイルします。
境界とツールの強制
モノレポが数十から数百のパッケージに成長するにつれて、アーキテクチャ上の境界を強制することが絶対的に重要になります。フロントエンドのWebアプリケーションが、バックエンドのユーティリティパッケージから機密性の高いデータベース接続文字列を直接インポートすることは望ましくありません。
eslint-plugin-workspacesのようなツールや、特殊な境界強制ルールを使用することで、依存関係が単一の、期待される方向にのみ流れるようにすることができます。この厳密な強制と、Turborepoのリモートキャッシュを活用した堅牢なCI/CDパイプラインを組み合わせることで、強力なアーキテクチャが実現します。
リモートキャッシュは、おそらくTurborepoの最も革新的な機能です。Turborepoのキャッシュをエンジニアリングチーム全体と継続的インテグレーション環境で共有することで、誰か(またはCIサーバー)がパッケージをビルドした場合、他の誰もそれを再ビルドする必要がなくなります。大規模な時間短縮は天文学的であり、数分かかっていたビルドが数秒のキャッシュヒットに変わります。
結論
現代のTypeScriptモノレポを構築することは、もはや専用の開発者エクスペリエンスチームを持つ巨大なテクノロジー企業だけのものではありません。pnpmワークスペースの厳格さとTurborepoの驚異的な速度を活用することで、あらゆる規模のチームが、高い凝集性、無限の拡張性、そして開発が非常に楽しいコードベースを構築できます。単純なチュートリアルを超えて、これらの高度なアーキテクチャパターンを採用し、エンジニアリングの速度が新たな高みに到達するのを見てください。
こちらもおすすめ
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

エンタープライズアプリケーションのためのTypeScript高度パターン
branded type、条件付き応答型、テンプレートリテラルルーティング、satisfies演算子など、エンタープライズ向けTypeScriptの高度なパターンを習得しましょう。
Read more
高度なGraphQLフェデレーション: 分散型スーパーグラフの構築
Apollo Federation v2を用いたエンタープライズ向け分散型GraphQL APIの構築方法を学び、サブグラフのentity resolution、スキーマ構成、@keyディレクティブ、キャッシング戦略、パフォーマンス最適化を伴うゲートウェイルーティングを習得しましょう。
Read more
TurborepoモノレポアーキテクチャにおけるJestからVitestへの移行
TypeScript TurborepoモノレポでJestテストスイートをVitestへ移行し、実行時間を10倍高速化するための完全なステップバイステップガイド。
Read more