lerna/yarn/npm workspacesとTypeScript Project Referencesの設定を同期するツール
TypeScript Project Referencesは、tsconfig.json同士の依存関係を定義することで、効率的なビルドが可能になる仕組みです。
Project Referencesの仕組みを使うことで、monorepoなど一つのリポジトリ内で複数のTypeScriptで書かれたパッケージがある場合に効率的なインクリメンタルビルドなどができます。 また、VSCodeなどのコード補完に使われるTS-Serverなどのスキャンも効率的になります。
- Migrating Large TypeScript Codebases To Project References — Developer Tooling
- Optimizing multi-package apps with TypeScript Project References | by Mirko Kruschke | eBay Tech Berlin
たとえば、次のような構造で packages/front は packages/common に依存しているとします。
.
└── packages/
├── front/
├── server/
└── common/
この場合にTypeScript Project Referencesで依存関係を表現するなら、packages/front の references
にCommonへの相対パスを指定します。
// packages/front/tsconfig.json
{
"references": [
{"path": "../common"}
]
}
ビルドする場合には、tsc -p .
ではなく tsc --build .
というように --build
フラグを使ってビルドします。
また、各tsconfig.jsonには composite
と declaration
が必須となるなどの制限もあります。
詳しくはドキュメントを参照してください。
このTypeScript Project Referencesは便利は便利なのですが、設定が手間であるという問題が知られています。
多くのmonorepoではLerna、Yarn’s workspaces、npm’s workspacesなどを利用していると思います。
このworkspaceの設定と重複するreferences
の設定をtsconfig.json
に手書きするのはあまり現実的ではありません。
そのため、monorepoのworkspaceの設定からtsconfig.json
のreferences
の設定を更新/チェックする@monorepo-utils/workspaces-to-typescript-project-referencesというツールを書きました。
@monorepo-utils/workspaces-to-typescript-project-references
似たようなツールはすでに何個もあるのですが、自分のニーズを満たせるものがなかったので作っています。
特徴としては次のようになっています。
- monorepoのworkspace設定とTypeScript’s Project Referencesの同期とテスト
tsconfig.json
のコメントは保持する- プラグインで任意のworkspaceツールに対応
インストール
npmなどを使ってインストールしてください
npm install @monorepo-utils/workspaces-to-typescript-project-references
使い方
CLIツールなのでHELPを見れば大体の使い方が分かります。
Usage
$ workspaces-to-typescript-project-references
Options
--root [Path:string] Root directory of the monorepo.
Default: current working directory
--check If set the flag, check only differences of tsconfig.json and does not update tsconfig.json.
If the check is failed, exit status 1. It is useful for testing.
--plugin [Path:string] Path to plugin script.
Load the plugin script as module and use it.
--tsconfigPath [Path:string] Use alternative config path inside the package. e.g.: tsconfig.test.json
Default: tsconfig.json
Examples
# Update project references in tsconfig.json
$ workspaces-to-typescript-project-references
# Test on CI
$ workspaces-to-typescript-project-references --check
Examples
このツール自身がmonorepo-utilsというmonorepoで管理されています。 monorepo-utilsはlerna + yarn workspacesを使っています。
次の箇所にworkspacesの設定が書かれていることが分かります。
"workspaces": {
"packages": [
"packages/*",
"packages/@monorepo-utils/*"
]
},
このmonorepo内で@monorepo-utils/workspaces-to-typescript-project-references
パッケージの依存関係を見てみると、@monorepo-utils/package-utilsという別のパッケージに依存しています。
"dependencies": {
"@monorepo-utils/package-utils": "^2.2.0",
"comment-json": "^3.0.3",
"meow": "^7.1.1"
}
そのため、workspaces-to-typescript-project-references
コマンドを実行すると、
@monorepo-utils/workspaces-to-typescript-project-references
のtsconfig.jsonに次のようにパスが追加されます。
"references": [
{
"path": "../package-utils"
}
]
これで、package.jsonを変更した際に workspaces-to-typescript-project-references
コマンドを実行するだけで、常にtsconfig.json
の定義を維持できます。
また、CIで workspaces-to-typescript-project-references --check
を実行すれば、package.jsonとtsconfig.jsonのズレが出た場合に気付けるようになっています。
Plugin
@monorepo-utils/workspaces-to-typescript-project-referencesの特徴として、プラグインで任意のworkspaceに対応できるようになっています。 この機能を持ってるツールがなかったので、このツールを書いたところはあります。
たとえば、Boltというツールもworkspaceを持っていますが、package.json
にbolt.workspaces
というフィールドを定義する独自の仕様です(yarn
やnpm
やルート直下のworkspace
になっている)
このBoltの対応は、次のようなbolt-plugin.js
を書くことで対応できます。
(get-monorepo-packagesというライブラリはBoltをサポートしているので、これを使うだけです。)
bolt-plugin.js
:
const getPackages = require("get-monorepo-packages");
const plugin = (options) => {
const monorepoPackages = getPackages(options.rootDir);
return {
supports() {
return monorepoPackages.length > 0;
},
getAllPackages() {
return monorepoPackages;
},
getDependencies(packageJSON) {
const dependencies = Object.entries(packageJSON.dependencies ?? {});
const devDependencies = Object.entries(packageJSON.devDependencies ?? {});
return [...dependencies, ...devDependencies].map((dep) => {
return {
name: dep[0]
};
});
},
resolve({ name }) {
const matchPkg = monorepoPackages.find((info) => {
return info.package.name === name;
});
if (!matchPkg) {
return null;
}
return matchPkg.location;
}
};
};
module.exports.plugin = plugin;
プラグインを使うときは、--plugin
に plugin.js
のパスを渡すだけです。
$ npm install @monorepo-utils/workspaces-to-typescript-project-references -g
$ workspaces-to-typescript-project-references --plugin ./bolt-plugin.js
おわりに
TypeScriptで書かれた大きなプロジェクトではTypeScript Project Referencesを使うことでコンパイル速度などが最適化できます。
大きなTypeScriptのプロジェクトは増えていると思うので、Project Referencesも使われることが増えていると思います。 しかし、現状のProject Referencesは使い勝手があまりよい感じのものではありません。
@monorepo-utils/workspaces-to-typescript-project-referencesはProject Referencesを補助するツールですが、将来的にTypeScript自体でもうちょっと良い感じに使えるようになるのがいい気がします。
- Infer project references from common monorepo patterns / tools · Issue #25376 · microsoft/TypeScript
- https://twitter.com/azu_re/status/1310057006909149185
Project Referencesを使わないで paths
のaliasを使って別パッケージのtsファイルを直接参照してビルドするという裏技もありますが、かなり罠っぽい動作もあるので、素直にProject Referencesを使ったほうがまともだと思います。
textlintはTypeScriptで書かれているパッケージが多いので、Project Referencesと@monorepo-utils/workspaces-to-typescript-project-referencesを使うようになっています。
TypeScript Project Referencesを現状使うべきかは迷うような感じではありますが、Project Referencesを使うにはtsc --build
でビルドする必要があるという制約があります。
そのため、Project Referencesを入れると必然的にTypeScriptのビルドキャッシュの仕組みが導入されるという副作用があります。
tsc --build
を使うと.tsbuildinfo
を使ったビルドのファイルキャッシュが作られます。
これは、すでにビルドキャッシュがあると一から再ビルドしなくてすむので、monorepoのようなビルドステップが複雑になりがちな場所では結構有用です。
Project Referencesはまだ実例があんまり多くなかったりエコシステム側の対応がいまいちという部分はありますが、徐々に良くなっているので触れる機会は増えるのかもしれません。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。