TypeScriptのプロジェクトで、なぜかローカルではコンパイルが通るけど、CIではコンパイルがコケる問題が発生しました。
問題が発生した状況
CIでコンパイルしているが、そこでコケる
- このプロジェクトでは、CIで
tsc
を実行して型チェックを行っています。 - CIを回したら、「error TS2503: Cannot find namespace 'NodeJS'」というエラーが発生しました。
- これは、
NodeJS
という型が見つからないというコンパイルエラー。
- これは、
ローカルではコンパイルが通る
- ローカル開発環境で
tsc
するとCIで発生しているようなコンパイルエラーは発生しませんでした。- 「僕の環境だと動くんですけど」状態
ローカル開発環境のディレクトリ構成
この問題の原因はローカル開発環境のディレクトリ構成が関わっていました。
ローカル開発環境では、~/projects
にプロジェクトごとにディレクトリを作る構成になっています。projects
ディレクトリは、複数のプロジェクトを置くだけのスペースで、これをGitHubにpushしたりはしていません。あくまでプロジェクトを整理するために設けたディレクトリです。
今回問題が起こったプロジェクトは~/projects/myproj
です。
~/projects
├── myproj # 問題の起こったプロジェクトのプロジェクトルート
│ ├── .git
│ ├── index.ts # ❶
│ ├── node_modules
│ │ └── @types # ❷
│ │ └── lodash
│ └── tsconfig.json
├── node_modules
│ └── @types
│ └── node # ❸
├── other_project1 # ┐
├── other_project2 # ├ 他のプロジェクトのディレクトリたち(今回は関係ない)
└── other_project3 # ┘
index.ts(❶)では、概ね次のようなNodeJS.ProcessEnv
の型を参照するコードがあります。
type A = NodeJS.ProcessEnv;
myproj
が単体で、index.tsが正しくコンパイルされれるためには、~/projects/myproj/node_modules/@types/node
があるべきでしたが、インストールされていませんでした(❷)。
代わりに、myproj
ディレクトリのひとつ上のディレクトリprojects
になぜかnode_modules
があり、その中に@types/node
がインストールされていました(❸)。
この~/projects/node_modules/@types/node
があるため、myproj
に@types/node
が無くても、index.ts
は問題なくコンパイルがされていました。
一方で、CIの環境にはプロジェクトルートより上のディレクトリにnode_modules
は存在していないため、@types/node
が見つけられず、コンパイルエラーになっていました。
TypeScriptのモジュール解決
TypeScriptのモジュール解決は、細かく言うといろいろあるのですが、ざっくりいうとNode.jsのモジュール解決を模倣するモードがあります。
そのモードでは、@types/node
型定義ファイルを探す場合、次の順でパスを登っていって探します。
- /path/to/projects/myproj/node_modules/@types/node
- /path/to/projects/node_modules/@types/node
- /path/to/node_modules/@types/node
- /path/node_modules/@types/node
- /node_modules/@types/node
この順で見つからなければコンパイルエラーになります。今回のCIでコンパイルエラーになったのは、5まで登っても@types/node
が見つからなかったためです。
myproj
にちゃんと@types/node
をインストールしていれば、ステップ1で型定義ファイルが見つかりコンパイルが通っていたはずです。
今回、ローカルでコンパイルエラーにならなかったのは、ステップ2の@types/node
が採用されていたためです。
一時的な解決策
一時的な対処は、myproj
に@types/node
をインストールするだけです。
根本的な解決策
こういうミスが二度と起きないようにするには、不必要なnode_modulesを上のディレクトリに作らないようにすることです。これは個人的に行える対策です。適当なファイルをnode_modules
という名前で作っておけば、うっかりnpm install
やyarn install
を行っても処理が失敗します。
echo 'ここにはnode_modules作らない' > ~/projects/node_modules
プロジェクト単位ではtypeRoots
を明示的に設定したほうが良さそうです。
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types"],
}
}
これを設定しておくと、型定義ファイルを探すのはプロジェクト配下のnode_modules/@types
だけになり、../node_modules/@types
、../../node_modules/@types
、...といった登って探すといったことが行われなくなります。