Edited at

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

*こちらは Build Battle Saga ~ Frontend ~ の発表資料になります



自己紹介

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


  • GitHub

  • Twitter

  • フロントエンドの人だけどスクラムマスター・インフラ・サーバーサイドといろいろやります

  • 今はAWS/Rails/Reactなプロダクトのテックリードやりつつ社内アジャイル相談窓口とか社内フロント講師(?)やってます



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



monorepoとは?どんなときに使うのか?


  • babelとかAngularとかがやってるやつ

  • ライブラリなどで、パッケージとしては異なるが、リリースサイクルが同じプロダクトを1つのリポジトリで管理しよう的なやつ

  • モノレポの実現のために、 yarn workspacelerna などがよく利用される



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を利用します




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




  • references で参照するtsプロジェクトにするために必要なオプションとcreate-react-appの必須オプションが競合した



    • isolatedModules: truedeclaration: true が両立できない


    • composite: trueにするためには declaration: trueが必要

    • create-react-appでは isolatedModule: trueが必要

    • →😇





微妙なところ


  • 素直に tsc -b [全プロジェクト] とすると、functionsから shared/lib/esのimportがあったとき(またはその逆)にトランスパイルエラーにならない


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はあるので、時間が解決してくれそう?



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



おわりに

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



Happy Hacking