破綻したサブモジュール構成を捨てて、pnpm + Turborepo でモノレポ化した話
2026-02-27
azblob://2026/02/27/eyecatch/2026-02-27-git-repository-monorepo-000.jpg

はじめに

今回は、複数リポジトリ + Git サブモジュールで管理していたプロジェクトを、pnpm ワークスペース + Turborepo のモノレポ構成に移行した話を書きます。

「サブモジュール、なんとなく導入したけど全然うまくいってない……」という方には、わりと刺さる内容かもしれません。


そもそもなぜモノレポ化が必要だったのか

移行前の構成

プロジェクトの構成はこんな感じでした。

【リポジトリ①】 service-admin-web   ← 管理画面(React)
【リポジトリ②】 service-api         ← バックエンドAPI(NestJS)
【リポジトリ③】 service-liff        ← ユーザー向けフロントエンド(React)

【リポジトリ④】 service-monorepo    ← ↑の3つをサブモジュールで束ねる想定だった
    ├── .gitmodules
    ├── admin-web/       ← 空ディレクトリ(未初期化)
    ├── api/             ← 空ディレクトリ(未初期化)
    └── liff/            ← 空ディレクトリ(未初期化)

……お気づきでしょうか。サブモジュールが全部空です。

git submodule status を叩くと、全部に - プレフィックスがついていて「未初期化」状態。つまり、モノレポ用のリポジトリは存在するけど、中身は空っぽ。実際の開発は外部リポジトリ3つで個別にやっている、という状態でした。

何が辛かったか

この構成、開発していると地味にストレスが溜まります。

1. 同じ変更を2箇所で意識しないといけない

API の型定義を変えたら、フロント側でも同じ変更をしないといけない。でもリポジトリが別だから、PR も別、マージタイミングも別。「API 側はマージしたけど、フロント側のPRがまだレビュー中」みたいな中途半端な状態が日常的に発生していました。

2. 依存パッケージがバラバラ

3つのリポジトリそれぞれに package.json があって、React のバージョンが微妙に違ったり、axios のバージョンが揃ってなかったり。「あれ、こっちでは動くのに向こうでは動かない」が起きるやつです。

3. 型の共有ができない

API のレスポンス型を TypeScript で定義しても、フロント側にコピペするしかない。もちろん、片方だけ更新して不整合が起きるのは時間の問題でした。


モノレポ化の方針

色々と調べた結果、以下の方針で進めることにしました。

  • pnpm ワークスペース でパッケージ管理を統一
  • Turborepo でビルド・開発サーバーのオーケストレーション
  • Git の履歴は保持 する(git filter-repo を使用)
  • 共有パッケージ(@project/shared)を作って型定義を一元管理

Yarn でも npm でもなく pnpm を選んだのは、ワークスペースの仕組みがシンプルで、かつディスク使用量が圧倒的に少ないからです。Turborepo はキャッシュが強力で、変更のあったパッケージだけビルドしてくれるのが決め手でした。


実際にやったこと

ステップ1:サブモジュールを消す

まずは使われていないサブモジュールを整理するところから。

cd service-monorepo

# サブモジュールの登録を解除
git submodule deinit -f admin-web api liff
git rm -f admin-web api liff
rm -rf .git/modules/*

# コミット
git commit -m "Remove unused submodules"

この瞬間、.gitmodules が消えてリポジトリがスッキリします。ここまでは簡単。

ステップ2:外部リポジトリの履歴を保持したまま統合する

今回こだわったのが Git 履歴の保持 です。

単純にファイルをコピーするだけなら rsyncrobocopy で一瞬なんですが、それだと過去のコミット履歴が全部消えてしまう。「あのとき何でこの実装にしたんだっけ?」と git log を辿りたい場面は確実に来るので、履歴は残したかった。

そこで使ったのが git filter-repo です。

# 1. 外部リポジトリのミラーコピーを作る(原本には絶対触らない)
git clone --mirror ../service-api service-api-mirror
git clone service-api-mirror service-api-work --no-local

# 2. filter-repo でパスを書き換える
cd service-api-work
git filter-repo --to-subdirectory-filter api
# → 全ファイルが api/ 配下に移動。履歴もすべて書き換わる

# 3. モノレポ側でリモートとして追加してマージ
cd ../service-monorepo
git remote add api-migrated ../service-api-work
git fetch api-migrated
git merge --allow-unrelated-histories api-migrated/develop

--allow-unrelated-histories がポイントで、もともと関係のない2つのリポジトリの履歴を強制的にマージしてくれます。

ここでコンフリクトが起きることもありますが、今回はモノレポ側に空ディレクトリがあっただけなので、削除して git add -A で解消しました。

この手順を admin-web、liff にも繰り返して、全リポジトリの履歴付き統合が完了です。

ちなみに: git filter-repo は Python 製のツールで、pip install git-filter-repo でインストールできます。古い git filter-branch よりも圧倒的に速くて安全なので、こちらを使うのがおすすめです。

ステップ3:pnpm ワークスペースの設定

統合が終わったら、pnpm のワークスペースを設定します。

# pnpm-workspace.yaml
packages:
  - 'packages/*'      # 共有パッケージ
  - 'admin-web'
  - 'api'
  - 'api/packages/*'  # API内部のサブパッケージ
  - 'liff'

ルートで pnpm install を叩くと、全パッケージの依存関係がまとめて解決されます。

ステップ4:パッケージのバージョン統一

ここが地味に大変でした。3つのプロジェクトで微妙にバージョンが違うパッケージを洗い出して統一していきます。

# 実際に統一したもの(一部)
React:       17.0.2 / 18.2.0 / 18.2.018.2.0 に統一
TypeScript:  5.3.0 / 5.6.2 / 5.5.05.6.2 に統一
axios:       1.6.0 / 1.7.8 / 1.7.21.7.8 に統一

バージョンを揃えたら pnpm installpnpm dedupe で重複を排除。node_modules のサイズがけっこう減って気持ちよかったです。

ステップ5:共有パッケージの作成

モノレポ化の旨みはここからです。packages/shared を作って、API とフロントで共有する型定義を置きます。

packages/
└── shared/
    ├── package.json
    ├── tsconfig.json
    └── src/
        ├── index.ts
        ├── types/        # 共通の型定義
        └── utils/        # 共通のユーティリティ
// packages/shared/package.json
{
  "name": "@project/shared",
  "version": "1.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts"
}

これで、フロント側からこんな風にインポートできるようになります。

import { UserInfo, formatDate } from '@project/shared';

API 側で型定義を変えたら、フロント側でも自動的に型エラーが出る。コピペ不整合とはおさらばです。

ステップ6:Turborepo の導入

Turborepo を入れると、ルートから全パッケージを一括で操作できるようになります。

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    }
  }
}

ルートの package.json にスクリプトを追加して、こんな感じで使えるようにしました。

pnpm dev          # 全パッケージの開発サーバーを並行起動
pnpm dev:api      # API だけ起動
pnpm dev:admin    # 管理画面だけ起動
pnpm build        # 全パッケージビルド
pnpm test         # 全パッケージテスト

pnpm dev 一発で API もフロントも全部立ち上がるのは本当に楽です。

ステップ7:CI/CD の再設定

GitHub Actions のワークフローも書き直しました。ポイントは2つ。

1. pnpm のキャッシュを効かせる

- name: Setup pnpm cache
  uses: actions/cache@v3
  with:
    path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

2. Turborepo のキャッシュも効かせる

- name: Setup Turbo cache
  uses: actions/cache@v3
  with:
    path: .turbo
    key: ${{ runner.os }}-turbo-${{ github.sha }}

この2つのキャッシュのおかげで、2回目以降の CI は劇的に速くなりました。


モノレポ化で変わったこと

Before

  • API の型を変更 → フロントに手動コピー → コピー忘れでバグ
  • 3リポジトリに別々に PR → レビューが分散 → マージ順の調整が面倒
  • pnpm install を3箇所で実行 → 同じパッケージが3重にダウンロード
  • CI が3本走る → 全部通るのを待ってからデプロイ

After

  • 型定義は @project/shared に一元管理 → 変更したら即座に型エラーが出る
  • 1つの PR で API もフロントも変更 → 原子的なコミットが可能
  • pnpm install 1回で全部入る → ディスク使用量も削減
  • CI は1本 → Turborepo のキャッシュで差分ビルド

ハマったポイント

1. git filter-repo のインストール

macOS だと Homebrew で入るんですが、Windows だと pip で入れる必要があります。しかも PATH が通ってないことがあるので、そこで少しハマりました。

pip install git-filter-repo
# PATH が通らない場合
python -m git_filter_repo --help

2. マージコンフリクト

--allow-unrelated-histories でマージすると、モノレポ側に同名のディレクトリがある場合にコンフリクトが起きます。今回はサブモジュールの残骸(空ディレクトリ)が原因でした。Git が退避用に api~HEAD みたいなディレクトリを作ってくるので、それを消して解決。

3. 共有パッケージのビルド順

@project/shared を API やフロントから参照している場合、先にビルドしないとインポートが解決できません。Turborepo の dependsOn: ["^build"] がこれを自動でやってくれるんですが、最初は設定を忘れていて「なんで型が見つからないんだ?」としばらく悩みました。

4. 「勝手にデプロイされるのだけは怖い」問題

CI/CD を再設定するとき、push トリガーのワークフローが意図せず走ってしまうのが一番怖かったです。最終的には dry-run モードでデプロイフローの確認をしてから、本番適用しました。


振り返り

正直、モノレポ化の作業自体は 1日で完了 しました。ただし、それは手順書を事前に作り込んでいたからで、調査・検討の期間を含めると約1週間です。

特に git filter-repo を使った履歴保持マージは、事前にローカルで何度もリハーサルしたおかげで本番はスムーズにいきました。「リモートには絶対に push しない」「原本のリポジトリには絶対に触らない」を徹底して、ミラーコピーの上で作業するのが精神衛生上よかったです。

サブモジュールで中途半端にモノレポ「風」にするくらいなら、最初から pnpm ワークスペースで真のモノレポにした方が圧倒的に楽です。


まとめ

  • サブモジュールが形骸化しているなら、早めにモノレポへ移行した方がいい
  • git filter-repo を使えば、履歴を保持したままリポジトリを統合できる
  • pnpm ワークスペース + Turborepo の組み合わせは、中小規模のプロジェクトにちょうどいい
  • 共有パッケージを作ることで、型安全性とDXが大幅に向上する
  • 事前に手順書とリハーサルをやっておけば、本番移行は意外とスムーズ

モノレポ化を検討している方の参考になれば嬉しいです。


参考リンク