メインコンテンツまでスキップ

Monorepoとは?npm workspacesを使ったクロスプロジェクトコード共有の完全ガイド

· 約9分
AI MDX 編集

現代のフロントエンドおよびフルスタック開発において、プロダクトが拡大するにつれて、「複数のプロジェクトで同じコードベースを共有する必要がある」という状況によく直面します。例えば、ユーザー向けのメインサイト(Client App)と内部スタッフ向けの管理画面(Admin Panel)などです。これらは独立して稼働しますが、同じUIコンポーネントライブラリ、API呼び出しロジック、または型定義を共有することがよくあります。

もし両方のプロジェクトに同じコードをコピペしてしまうと、将来ロジックを変更する際に、エンジニアは複数のプロジェクトで何度も変更作業を行わなければならず、見落としやバージョンの不一致が生じるリスクが高まります。この問題を解決するために、**Monorepo(モノレポ)**アーキテクチャが登場しました。そして、現在のNode.jsエコシステムにおいて、npm workspaces は最も導入しやすいツールの1つです。

Monorepoアーキテクチャとは?

Monorepo 概念解說圖

Monorepo(Monolithic Repository)とは、複数の異なるプロジェクトやパッケージを、すべて単一のGitリポジトリ内で管理することを指します。これと相対するのが従来のPolyrepo(Multi-repo)であり、各プロジェクトが独立したリポジトリを持つ構成です。

Monorepoの主なメリット

  1. 信頼できる唯一の情報源 (Single Source of Truth):すべてのコードが同じツリー構造の下にあり、チームメンバーが常に一貫したコードベースを参照できるようになります。
  2. コードの共有が簡単:クロスプロジェクトで共有モジュールを参照する際、ローカルでのシンボリックリンク (Symlink) を使うだけで済みます。テストのためにパッケージをいちいち npm registry に公開する必要はありません。
  3. 依存関係の一貫性:サードパーティの依存パッケージ(ReactやLodashなど)をルートディレクトリに巻き上げ (Hoist) することで、すべてのサブプロジェクトで使用されるパッケージバージョンを完全に一致させ、バージョンの競合を防ぎ、ストレージのスペースを節約できます。
  4. 大規模なリファクタリングが容易:共有モジュールのAPIが変更された場合、そのモジュールに依存するすべてのプロジェクトが同じリポジトリ内にあるため、エンジニアは一度に変更を行い、TypeScriptコンパイラを使って潜在的なエラーをすべて検出できます。

npm workspacesについて

npm v7 バージョン以降、npm は公式に Workspaces のサポートを組み込みました。これは、同じローカルファイルシステム内の複数のパッケージの依存関係を管理するためのCLIネイティブ機能を提供します。ルートディレクトリの package.json を設定するだけで、npm は自動的にサブプロジェクトの依存関係を整理し、相互参照できるようにしてくれます。

実践チュートリアル:npm workspacesを使ってコードを共有する方法

具体的な例を通して、project-aproject-b、および共有モジュール shared-utils を含む Monorepo を構築する方法を説明します。

ステップ1:ルートディレクトリの作成と初期化

まず、Monorepo のルートディレクトリとして新しいフォルダを作成し、プロジェクトを初期化します。

mkdir my-monorepo
cd my-monorepo
npm init -y

次に、生成されたルートディレクトリの package.json に手動で workspaces フィールドを追加します。これにより、この Monorepo がどのディレクトリにサブプロジェクトを含んでいるかを宣言します。通常、プロジェクトは apps(アプリケーション)と packages(共有モジュール)に分類します。

package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}

注意:誤ってワークスペース全体をパブリックのnpmレジストリに公開しないように、ルートディレクトリの package.json には必ず "private": true を設定してください。

ステップ2:共有モジュール (Shared Package) の作成

それでは、共有ロジックを配置するパッケージを作成しましょう。ルートディレクトリ配下に packages/shared-utils フォルダを作成します。

mkdir -p packages/shared-utils
cd packages/shared-utils
npm init -y

packages/shared-utils/package.json を編集します。特に "name" フィールドに注意してください。これが他のプロジェクトからこのモジュールを参照する際に使用される名前になります。

packages/shared-utils/package.json
{
"name": "@my-org/shared-utils",
"version": "1.0.0",
"main": "index.js"
}

次に、そのフォルダ内に index.js を作成し、共有したい関数を記述します。

packages/shared-utils/index.js
// 共有の日付フォーマット関数
function formatDate(date) {
return new Intl.DateTimeFormat('ja-JP', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).format(date);
}

// 共有の足し算関数
function add(a, b) {
return a + b;
}

module.exports = {
formatDate,
add
};

ステップ3:2つのアプリケーションプロジェクトの作成

ルートディレクトリに戻り、apps/ ディレクトリ配下に2つの独立したプロジェクトを作成します(ここでは簡略化のため、基本的な Node.js プロジェクトを初期化しますが、実務では Next.js や React、Express などのプロジェクトになります)。

mkdir -p apps/project-a
mkdir -p apps/project-b

# project-a の初期化
cd apps/project-a
npm init -y

# project-b の初期化
cd ../project-b
npm init -y

それぞれ apps/project-a/package.jsonapps/project-b/package.json"name""project-a""project-b" に変更します。

ステップ4:共有モジュールをプロジェクトにインストール

ここからが最も重要なステップです。作成した @my-org/shared-utilsproject-aproject-b で使用できるようにします。

npm workspaces 環境では、相対パス (../../packages/shared-utils) を手動で設定する必要はありません。npm のインストールコマンドと -w (workspace) パラメータを使用します。

# Monorepoのルートディレクトリで実行
npm install @my-org/shared-utils -w project-a
npm install @my-org/shared-utils -w project-b

実行後、project-apackage.json に依存関係が追加されていることがわかります。

apps/project-a/package.json
"dependencies": {
"@my-org/shared-utils": "^1.0.0"
}

この時、npm はインターネットから @my-org/shared-utils をダウンロードするのではなく、シンボリックリンク (Symlink) を利用して apps/project-a/node_modules/@my-org/shared-utils をローカルの packages/shared-utils ディレクトリに直接接続します。つまり、共有モジュールに加えた変更は、すべて即座に2つのアプリケーションに反映され、再コンパイルや再インストールは必要ありません。

ステップ5:プロジェクト内で共有コードを呼び出す

最後に、project-a で共有コードを適切に読み込めるかテストします。apps/project-a/index.js を作成します。

apps/project-a/index.js
const { formatDate, add } = require('@my-org/shared-utils');

const today = new Date();
console.log(`【Project A】今日は:${formatDate(today)}`);
console.log(`【Project A】計算結果 10 + 20 = ${add(10, 20)}`);

ルートディレクトリに戻り、実行します。

node apps/project-a/index.js

フォーマットされた日付の文字列と計算結果が正しくコンソールに出力されれば、クロスプロジェクトでのコード共有の実装は完了です!

高度なテクニックとベストプラクティス

基本的なコード共有に加えて、大規模な Monorepo を運用するためには、以下のようなベストプラクティスを把握しておくことをお勧めします。

1. 依存パッケージの一元管理 (Dependency Hoisting)

従来の Polyrepo では、各プロジェクトが巨大な node_modules を抱えていました。しかし npm workspaces では、すべてのサブプロジェクトで「共通」のサードパーティ依存関係がデフォルトで「ルートディレクトリ」の node_modules に引き上げられます(Hoisting)。これにより、依存関係のバージョンの地雷(例えばReactの二重インスタンス化など)が解消されるだけでなく、インストール時間やディスク容量も大幅に削減されます。ルートディレクトリで一度 npm install を実行するだけで準備が整います。

2. ビルドツールとの統合 (Turborepo)

npm workspaces によって依存関係のインストールの問題は解決しましたが、「テスト」や「ビルド」などのスクリプトを実行する際、各ディレクトリに移動して手動で実行するのは非常に非効率です。 実務においては、TurborepoLerna といったビルド自動化ツールとの組み合わせを強く推奨します。特に Turborepo は「クラウ​​ドキャッシュ」と「並行実行 (Concurrency)」の機能を備えています。各サブプロジェクト間の依存性のトポロジーグラフを自動で分析し、shared-utils のビルドが完了した瞬間にそれに依存する project-a のビルドを開始することで、ビルド時間を限界まで短縮してくれます。

3. TypeScript のパスマッピング (Path Mapping)

プロジェクトで TypeScript を採用している場合、ルートディレクトリに tsconfig.base.json を設計し、references (Project References) の仕組みを活用して、共有パッケージの型定義を再利用させることを忘れないでください。これにより、開発体験 (IDEでのソースコードジャンプのサポート) が向上するだけでなく、メモリの爆発を防ぐことができます。

結論

Monorepo と npm workspaces の導入は、複数の関連プロジェクトを持つチームにとって、現代のフロントエンドエンジニアリングにおいて避けて通れない大きな道です。初期段階では ESLint や TypeScript、CI/CD パイプラインの構成調整に少し時間がかかるかもしれませんが、それと引き換えに得られる「リファクタリングへの自信」、「厳密なバージョンの一貫性」、そして「極めて高いコードの再利用性」は、投資する価値が十分にあります。

もし、あなたのプロジェクトが現在「コアロジックを修正するために複数のリポジトリを跨いでPull Requestを出さなければならない」という苦痛に悩まされているなら、今すぐ試しに小さな workspace を立ち上げてみてはいかがでしょうか?