やりたかったこと(症状)

社内のinternal-dashboardリポジトリを新しいマシンにセットアップしようとした。このリポジトリは共通UIコンポーネント群をpackages/ui-kitというsubmoduleとして参照している。

git clone git@github.com:example-org/internal-dashboard.git
cd internal-dashboard
npm install
npm run build

npm installまでは問題なく終わったが、npm run buildでビルドが止まった。

[vite]: Rollup failed to resolve import "../../packages/ui-kit/dist/index.js" from "src/App.tsx".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`

packages/ui-kitというディレクトリ自体は存在していたので、最初はビルド設定側の解決パス(vite.config.tsのエイリアス設定)がおかしいのだと思い込み、そちらを疑った。

環境

  • OS: Ubuntu 24.04 LTS
  • Git: 2.45.2
  • Node.js: v20.14.0
  • npm: 10.7.0
  • リポジトリ: internal-dashboard(GitHub、submoduleとしてpackages/ui-kitを参照)
  • ビルドツール: Vite 5.3(Astroではなく素のReact+Viteプロジェクト)

試したこと

まずvite.config.tsのエイリアス設定を確認したが、パス自体は正しかった。次に、packages/ui-kitの中身を直接見てみた。

ls -la packages/ui-kit
total 8
drwxr-xr-x  2 user user 4096 Aug 19 10:02 .
drwxr-xr-x 12 user user 4096 Aug 19 10:02 ..

ディレクトリはあるのに中身が一切ない。ここで初めて「ビルド設定の問題ではなく、そもそもファイルが存在していない」ことに気づいた。

念のためgit statusも確認したが、何も異常を示さなかった。

git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

git statusが「clean」と報告するため、submoduleが未取得であることに気づきにくい。これに惑わされて、しばらくビルド設定側を疑い続けてしまった。

次に.gitmodulesの中身を確認した。

cat .gitmodules
[submodule "packages/ui-kit"]
	path = packages/ui-kit
	url = git@github.com:example-org/ui-kit.git
	branch = main

submoduleとしての定義自体は正しく存在している。ここでようやくgit submodule statusを実行してみた。

git submodule status
-4f2a9c1e8b3d7a6f5e4d3c2b1a0f9e8d7c6b5a4f packages/ui-kit

行頭に-(マイナス記号)が付いている。これがsubmodule未初期化を示すサインだった。

原因

git cloneは、対象リポジトリに.gitmodulesファイルと、submoduleが指す特定コミットへの参照(gitlink)だけを取得する。submoduleの実際のファイル内容までは、デフォルトでは取得しない。そのためpackages/ui-kitというディレクトリ自体はcloneの時点で作られるが、中身は空のまま残る。

git submodule statusの出力先頭に付く記号には意味があり、-は「未初期化(登録されているがローカルに一度もcheckoutされていない)」、+は「初期化済みだが、記録されているコミットとローカルの内容が食い違っている」、記号なしは「初期化済みかつ最新」を意味する。今回は-だったので、initすら行われていない状態だった。

さらに厄介なのは、git statusが通常このsubmodule未初期化を警告しない点だった。packages/ui-kitは追跡対象のgitlinkとして記録されているだけで、その配下のファイル状態はデフォルトのgit statusが関知する範囲外になる。結果として、リポジトリ自体は「clean」に見えるのに、ビルドだけが失敗するという分かりにくい状況が発生していた。

解決手順

1. submoduleの状態を確認する

git submodule status
-4f2a9c1e8b3d7a6f5e4d3c2b1a0f9e8d7c6b5a4f packages/ui-kit

-が付いていることを再確認し、未初期化だと確定させた。

2. submoduleを初期化して実体を取得する

git submodule update --init --recursive
Submodule 'packages/ui-kit' (git@github.com:example-org/ui-kit.git) registered for path 'packages/ui-kit'
Cloning into '/home/user/internal-dashboard/packages/ui-kit'...
Submodule path 'packages/ui-kit': checked out '4f2a9c1e8b3d7a6f5e4d3c2b1a0f9e8d7c6b5a4f'

--recursiveを付けているのは、ui-kit自体がさらに別のsubmoduleを持つ可能性を考慮したため(今回は実際にはネストしたsubmoduleはなかったが、癖として付けている)。

3. ディレクトリの中身を再確認する

ls packages/ui-kit
dist  package.json  src  tsconfig.json

空だったディレクトリに、期待通りのファイル一式が展開されていた。

4. submoduleの状態を再確認する

git submodule status
 4f2a9c1e8b3d7a6f5e4d3c2b1a0f9e8d7c6b5a4f packages/ui-kit (heads/main)

先頭の-が消え、記録されているコミットと一致していることが分かる。

5. ビルドを再実行する

npm run build
vite v5.3.1 building for production...
✓ 214 modules transformed.
dist/index.html                  0.46 kB
dist/assets/index-C8kQmZ1a.js  186.32 kB
✓ built in 3.12s

エラーなくビルドが完了した。

動作確認

念のため、まっさらなディレクトリに--recurse-submodulesを付けて再クローンし、初回からsubmoduleが実体として取得されることを確認した。

git clone --recurse-submodules git@github.com:example-org/internal-dashboard.git internal-dashboard-check
cd internal-dashboard-check
ls packages/ui-kit
dist  package.json  src  tsconfig.json

cloneの時点でpackages/ui-kitの中身が展開されていることを確認できた。

まとめ

  • git cloneはsubmoduleの定義(.gitmodules)と参照コミットだけを取得し、実際のファイル内容は取得しない。ディレクトリ自体は作られるため、lsで気づくまで見落としやすい。
  • git statusはデフォルトでsubmodule未初期化を警告しないため、「リポジトリはcleanなのにビルドだけ失敗する」という分かりにくい症状になる。git submodule statusを実行し、行頭に-が付いていないか確認するのが確実な切り分け方法。
  • 恒久対策として、新規cloneはgit clone --recurse-submodulesを使う。既存の作業ディレクトリで気づいた場合はgit submodule update --init --recursiveで取得できる。git config --global submodule.recurse trueを設定しておくと、以降のgit pullcheckoutでもsubmoduleが自動更新されるようになる。

よくある質問

Q: git pullしただけでもsubmoduleの中身は最新になりますか? なりません。git pullは親リポジトリが記録しているsubmoduleの参照コミットを更新するだけで、実際のsubmoduleの作業ディレクトリには反映されません。誰かがsubmodule側で新しいコミットを指すように更新した場合、こちら側でもgit submodule update(またはgit submodule update --remote)を実行する必要があります。

Q: 毎回--recurse-submodulesを付け忘れそうです。忘れないようにする方法はありますか? git config --global submodule.recurse trueを設定しておくと、clone以外にもpullcheckoutのタイミングでsubmoduleが自動的に追従するようになります。ただしチーム全員の環境で有効になるわけではないので、READMEにclone手順として明記しておくのも有効です。

Q: git submodule statusの記号(- +のあり/なし)の意味を毎回忘れます。 -は未初期化、+は初期化済みだが記録コミットとローカルの内容が食い違っている、記号なしは初期化済みかつ最新、と覚えておくと切り分けが早くなります。今回のようにgit statusがcleanと出るのに動作がおかしいときは、まずこのコマンドを疑うとよいです。

関連記事