あるTypeScriptプロジェクトから別のTypeScriptプロジェクトを参照できるようになった。巨大プロジェクトを分割することができ、以下の様なメリットがある。
- 結合度が低く部品化された設計ができる
- 一方向の可視性を定義できる (実装コードからテストコードを見えないようにする etc)
- 必要な箇所だけビルド(型チェック/トランスパイル)するためビルド速度が上がる
詳細は[Handbook - Project References](https://www.typescriptlang.org/docs/handbook/project-references.html)を参照。
### ビルドモード
TypeScript3.0ではプロジェクト単位でインクリメンタルビルドができるようになった。`tsc --build`または`tsc -b`とフラグを指定するだけ。
以下のようなプロジェクトがある場合を考える。
```ls
src
├── index.ts
├── tsconfig.json
└── util.ts
```
従来は[[tsc]]コマンドを何度実行しても、都度フルビルドをしていた。ビルドモードを使って必要な時のみビルドさせる。
#### 1度目の実行
1度目の実行では全てのtsファイルがビルドされる。
```shell
$ npx tsc -b -v
[20:56:16] Projects in this build:
* tsconfig.json
[20:56:16] Project 'tsconfig.json' is out of date because output file 'index.js' does not exist
[20:56:16] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...
```
`index.js`が存在しない時点で、このプロジェクトはリビルドが必要と判断されたからだ。もちろん`util.js`が無い場合も同じ。
#### 2度目の実行
2度目の実行では実際のビルド処理はスキップされる。
```shell
$ npx tsc -b -v
[20:58:44] Projects in this build:
* tsconfig.json
[20:58:44] Project 'tsconfig.json' is up to date because newest input 'index.ts' is older than oldest output 'util.js'
```
以下の関係にあるため、ビルドは必要ないと判断するからだ。
```
新しい
↑
最も古い成果物の`util.js`
最も古い入力ファイルである`index.ts`
↓
古い
```
#### ビルドモードの問題点
[[tsc]]ビルドするプロジェクトが別プロジェクトのソースを参照する場合、リビルド判定が正しく行われない。たとえば以下の構成を考える。
```ls
src
├── index.ts
├── tsconfig.json
└── util.ts
test <============== test配下で tsc -b する
├── index.ts <====== ../src/util.tsを参照している
└── tsconfig.json
```
`tsc -b`でビルドすると、`test/index.js`および参照先の`src/util.js`が生成される。
```ls
src
├── index.ts
├── tsconfig.json
├── util.js <===== 生成
└── util.ts
test
├── index.js <==== 生成
├── index.ts <==== ../src/util.tsを参照している
└── tsconfig.json
```
この状態で`util.ts`に変更を加えてもう一度`tsc -b`でビルドする。期待するのは`src/util.js`の再生成だが、`test`配下には変更がないためビルドは不要と判断される。
```ls
src
├── index.ts
├── tsconfig.json
├── util.js <===== ❹ リビルドされない(これはまずい)
└── util.ts <===== ❸ 変更を加えたのに。。
test
├── index.js <==== ❷ ビルドされない(これはOK)
├── index.ts <==== ❶ 変更されていないため..
└── tsconfig.json
```
これを解決するには必ず以下の順で処理をしなければいけない。
1. `src`配下で`tsc -b`
2. `test`配下で`tsc -b`
プロジェクト参照を使うと、2の手順のみでOKになる。
### プロジェクト参照
参照されるプロジェクト、参照するプロジェクトでそれぞれ設定が必要。
#### 参照されるプロジェクトの設定
ここでは`src/tsconfig.json`のこと。`comoposite: true`を追加する。
```diff
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
- "esModuleInterop": true
+ "esModuleInterop": true,
+ "composite": true
}
}
```
これには以下のような効果もある。
* 設定なしに別プロジェクトの参照を禁止する (`test`配下のファイルをimportできない)
* ビルド時に`.d.ts`ファイルを出力する
#### 参照するプロジェクトの設定
ここでは`test/tsconfig.json`のこと。`references: []`を追加する。
```diff
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
+ },
+ "references": [
+ { "path": "../src" }
+ ]
}
```
この設定で、ビルド時に`../src`のリビルド必要性を確認できるようになる。
#### tsc -b の動作確認
この状態で`test`にてビルドする。
```ls
src
├── index.ts
├── tsconfig.json
└── util.ts
test <========= tsc -b
├── index.ts
└── tsconfig.json
```
それぞれ`index.js`がないのでフルビルドされる。
```shell
$ npx tsc -b -v
[21:27:31] Projects in this build:
* ../src/tsconfig.json
* tsconfig.json
[21:27:31] Project '../src/tsconfig.json' is out of date because output file '../src/index.js' does not exist
[21:27:31] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...
[21:27:33] Project 'tsconfig.json' is out of date because output file 'index.js' does not exist
[21:27:33] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/test/tsconfig.json'...
```
そのままもう一度ビルドするとスキップされる。これは自明だ。`js`ファイルの他に`d.ts`ファイルや`tsconfig.tsbuildinfo`ファイルが増えている。
```ls
src
├── index.d.ts <============== d.tsファイルができている
├── index.js
├── index.ts
├── tsconfig.json
├── tsconfig.tsbuildinfo <==== ビルド情報 (差分判定に使う?)
├── util.d.ts <=============== d.tsファイルができている
├── util.js
└── util.ts
test
├── index.js
├── index.ts
└── tsconfig.json
```
先ほどのように`src/util.ts`を変更してもう一度コマンドを実行してみる。
```shell
$ npx tsc -b -v
[21:33:38] Projects in this build:
* ../src/tsconfig.json
* tsconfig.json
[21:33:38] Project '../src/tsconfig.json' is out of date because oldest output '../src/index.js' is older than newest input '../src/util.ts'
[21:33:38] Building project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...
[21:33:38] Updating unchanged output timestamps of project 'C:/Users/syoum/work/sandbox/typescript/project-reference/src/tsconfig.json'...
[21:33:38] Project 'tsconfig.json' is up to date with .d.ts files from its dependencies
[21:33:38] Updating output timestamps of project 'C:/Users/syoum/work/sandbox/typescript/project-reference/test/tsconfig.json'...
```
`Project '../src/tsconfig.json' is out of date because...`とあるよう.. 参照先の`src`プロジェクトが変更されたことを検知しリビルドされるようになった😄
TypeScript3.0時点ではインクリメントビルドはあくまでプロジェクト単位のみ。それでもプロダクトとテストを別プロジェクトにすれば、テストのたびにプロダクトをビルドする必要はなくなる。
また、プロダクトコードでテストモジュールを誤ってimportすることもなくなる。