*こちらは Build Battle Saga ~ Frontend ~ の発表資料になります
自己紹介
株式会社オプト シニアエンジニア @sisisin(しめにゃん)
- GitHub
- フロントエンドの人だけどスクラムマスター・インフラ・サーバーサイドといろいろやります
- 今はAWS/Rails/Reactなプロダクトのテックリードやりつつ社内アジャイル相談窓口とか社内フロント講師(?)やってます
TypeScriptのmonorepoでnode&browser向けtsconfig.json
monorepoとは?どんなときに使うのか?
- babelとかAngularとかがやってるやつ
- ライブラリなどで、パッケージとしては異なるが、リリースサイクルが同じプロダクトを1つのリポジトリで管理しよう的なやつ
- モノレポの実現のために、
yarn workspace
やlerna
などがよく利用される
monorepoとは?どんなときに使うのか?
→今回は、これをライブラリの提供ではなく、バックエンド〜フロントエンドまでフルtsなプロジェクトでも利用しちゃおうというときの話をします
想定するユースケース
firebaseプロジェクト
firebase functionsとフロントエンドでFirestoreへのアクセス(いわゆるRepository層)を共通化したい
サーバーサイドがtypescriptなプロジェクト
なんかロジック使いまわしたいかもしれない(単なるフロント・サーバーという構造で共通化したい層ってそんなないという説もありつつ)
ライブラリのexapmleコードをモノレポにしておくと便利そうじゃね?
exampleコードを更新する時、npmにpublishしてるやつを使うとなると事前にpublish必要だし、かといって相対パスでやるとハマった(ちゃんと調べてないですがなんか挙動が違った記憶がある)し・・・、モノレポになってればいいのでは!?
(モノレポライブラリを運用したことないので、トンチンカンなこといってるかもです。この辺詳しい人マサカリあればお願いします)
やってみる
- yarn workspace & lernaを利用したモノレポ
- 今回対応したときに使った技術
yarn@1.17.3
lerna@3.16.4
create-react-app@3.1.0
typescript@3.6.2
サンプルリポジトリ: https://github.com/sisisin-sandbox/monorepo-tsconfig
やりたいこと
- firebaseのFirestoreへのアクセスする処理をfirebase functionsとフロントで使いまわしたい
- 当然 type safeにやりたい
- 開発のときもなるべくサジェストなどのIDEサポートを受けたい
ディレクトリ構成
.
+-- packages/
| +-- front/ # フロントエンド
| +-- functions/ # firebase functions
| +-- shared/ # 共通処理
+-- package.json
TypeScriptのトランスパイル構成
-
Project References
を利用します- doc: https://www.typescriptlang.org/docs/handbook/project-references.html
- tsconfigを複数利用しながらも、トランスパイルは一元化できる仕組み
-
front/
とfunctions/
がそれぞれshared/
に依存する - →前者は ブラウザ環境 、後者はnode環境になる。そのための対応が必要
- 今回は
shared/lib/
配下にそれぞれに向けたトランスパイル結果を吐き出すように設定 -
shared/lib/cjs
,shared/lib/es
- 今回は
tsconfig/出力ファイル構成
.
+-- packages/
| +-- front
| +-- tsconfig.json
| +-- build/ # フロントビルド結果
| +-- functions/
| +-- tsconfig.json
| +-- lib/ # functionsビルド結果
| +-- shared/
| +-- tsconfig.base.json # shared用の共通tsconfig
| +-- tsconfig.node.json # node環境向けtsconfig
| +-- tsconfig.browser.json # browser環境向けtsconfig
| +-- lib/
| +-- cjs/ # node向けトランスパイル結果
| +-- es/ # browser向けトランスパイル結果
+-- package.json
このような構成で、以下のように tsc
コマンドを叩けばOK
フロント: tsc -b ./packages/shared/tsconfig.browser.json ./packages/front
functions: tsc -b ./packages/shared/tsconfig.node.json ./packages/functions
全部: tsc -b ./packages/shared/tsconfig.browser.json ./packages/shared/tsconfig.node.json ./packages/front ./packages/functions
できたこと
- TypeScriptの
Project References
を利用して全ディレクトリの型チェック・トランスパイルをサクッとやれる - 型を保ちながらバックエンドとフロントエンドで処理を共通化出来る
うまく行かなかったこと
- firestoreの型定義がブラウザ向けとfunctions向けで別物になっていて、ブラウザ向けは lib.dom.d.ts
へ依存しているなど、トランスパイル時に両方を満たすうまい方法が見つけられなかった
- import { firestore } from 'firebase'
と、 import { firestore } from 'firebase-admin'
でそれぞれ型定義が違うという話
- firebaseの話であってtsconfigの話とは微妙にズレるけど、ユースケースとして挙げてるのに出来てないじゃん、ということで。。
- tsconfigに "lib": ["dom"]
を追加したりして回避でいけなくはない、が・・・
まだ試していない案として、 FirestoreLike
のような型を用意して、この型の中身をtsconfig毎に差し替える、といった手もある、か、も、、、?(ほんとか?
class HogeRepository {
constructor(db: FirestoreLike){} // ←このFirestoreLikeがtsconfigの設定によって `firebase` と `firebase-admin`への参照に切り替わる、的な・・・?
}
↑の方法で倒せました https://qiita.com/sisisin/items/b2c12cd5f775e7312458
うまく行かなかったこと
-
rootディレクトリにtsconfig.jsonを配置して、
tsc -b .
- package.jsonで
tsc -b ./packages/a ./packages/b
のように列挙するよりはtsconfigにあったほうがわかりやすそうとおもって試したが以下の理由でダメだった
- package.jsonで
-
references
で参照するtsプロジェクトにするために必要なオプションとcreate-react-appの必須オプションが競合した-
isolatedModules: true
とdeclaration: true
が両立できない -
composite: true
にするためにはdeclaration: true
が必要 - create-react-appでは
isolatedModule: true
が必要 - →😇
-
微妙なところ
- 素直に
tsc -b [全プロジェクト]
とすると、functionsからshared/lib/es
のimportがあったとき(またはその逆)にトランスパイルエラーにならない
// functions/の中で↓これは実行時エラーになる(nodeはes modulesを解決できない)ので、トランスパイル時にエラーになってほしい
import { HogeRepository } from '../../shared/lib/es';
- functionsは
shared/lib/cjs
を使い、 frontはshared/lib/es
を使う想定だが、どちらもimport出来ちゃうので、これを抑止したいというモチベ - tsconfigのreferencesでそれぞれ参照するべきtsプロジェクトを設定してるはずなのに何故・・・
→ワークアラウンドとして、フロント向けとfunctions向けの tsc
コマンドをそれぞれ作り、これらを実行するときにトランスパイル結果を一度全部消すことに
(でかいプロジェクトになるとトランスパイル時間かかるはずなのでスケールするとなると微妙そう
微妙なところ
- create-react-appが素のままだと
src/
より親階層のファイルをimport出来ない- ので
react-app-rewired
を使ってModuleScopePlugin
を消し飛ばした - 一応workspace対応する issueはあるので、時間が解決してくれそう?
- ので
(単なるcreate-react-appへの愚痴になっちゃってますが
おわりに
というわけでnode/browser混じりのモノレポなTypeScriptプロジェクトをいいかんじにビルドするしたい話でした