TypeScriptのmonorepoでnode&browser向けtsconfig.json

\*こちらは [Build Battle Saga ~ Frontend ~](https://battle-saga-jp.connpass.com/event/142700/) の発表資料になります

---

# 自己紹介

株式会社オプト シニアエンジニア @sisisin（しめにゃん）

- [GitHub](https://github.com/sisisin)
- [Twitter](https://twitter.com/_sisisin)
- フロントエンドの人だけどスクラムマスター・インフラ・サーバーサイドといろいろやります
- 今は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毎に差し替える、といった手もある、か、も、、、？（ほんとか？~~

```typescript
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にあったほうがわかりやすそうとおもって試したが以下の理由でダメだった

- `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があったとき（またはその逆）にトランスパイルエラーにならない

```ts:index.ts
// 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はある](https://github.com/facebook/create-react-app/issues/3031)ので、時間が解決してくれそう？

(単なるcreate-react-appへの愚痴になっちゃってますが

---

# おわりに

というわけでnode/browser混じりのモノレポなTypeScriptプロジェクトをいいかんじにビルド~~する~~したい話でした

---

# Happy Hacking