yarnで始めるモノレポ構築

# はじめに

[TeQA](https://teqa.net/)というエンジニア向けサービスを開発しているのですが、その開発途中で得た知見を記載していこうと思います。

仲間と開発中に、js/tsをメインとするレポジトリで、サーバー・ウェブフロント・デスクトップのコードを１レポジトリで良い感じに管理したい、という話になりました。
下記のような構成をイメージしてもらえれば良いと思います。このようにコードをレポジトリ分割せず一括して１つのレポジトリで管理するものを、モノレポと呼んだりします。

```
- backend/
- front/
- desktop/
- common/
package.json
.prettierrc
.eslintrc.json
tsconfig.json
```


# モノレポなレポジトリへのアプローチ

色々と周辺技術を調べてみると、実現するために３つアプローチがあることがわかりました。
1. typescript3.0の[Project Reference](https://www.typescriptlang.org/docs/handbook/project-references.html) 
2. [Lerna](https://github.com/lerna/lerna)を利用
3. [yarn workspace](https://github.com/lerna/lerna)を利用

1に関して調査をしていくと、採用していたcreate-react-appはサポートしていないようです。GithubIssueは[こちら](https://github.com/lerna/lerna)
２に関して未知のツールを使うのはリソース的に厳しかったので、存在だけ認知し一旦見ませんでした。
３に関して、yarnのデフォルトツールで出来るなら良いじゃん！と思い、試しにプロトタイプを作ってみました。

# workspaceの構築
今回のソースコードは[ここ](https://github.com/yohachiSuga/ts-monorepo)にまとめてあります。
とりあえず下記の感じでフォルダ構成を作ってみます。

```
- packages/common
          /front
          /backend
          /desktop
```

ルートディレクトリで、

```
$ yarn init --yes --private
```
で出来上がったpackage.jsonにworkspaceフィールドを追加します。
これでpackagesフォルダ直下のフォルダがそれぞれworkspaceとみなされる環境が完成です。

```
{
  "name": "ts-monorepo",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "https://github.com/yohachiSuga/ts-monorepo.git",
  "author": "sakira <yohachi.suga@gmail.com>",
  "license": "MIT",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
}
```

Typescriptやライブラリを導入したいときルートディレクトリでワークスペース名を指定しつつ実行します。
例えば、

```
$ yarn workspace common tsc
```

実行後にcommon/node_modules/.bin/tscを見てみるとシンボリックリンクになっており、実体はルートディレクトリ直下のnode_modulesに配備されます。これによりモノレポの利点である「開発時の各プロジェクトのライブラリのファイルサイズ低減」が見込めます。

またeslintやprettierについては、それぞれのワークスペースに持たせずにルートディレクトリ自体に持たせます。このときワークスペース化しているので、ルートディレクトリのpackage.jsonにライブラリを入れるのにwarningが出ます。-Wオプションをつけて警告抑制をします。

```
$ yarn add -D -W eslint esl
int-config-airbnb eslint-config-prettier eslint-plugin-import eslint-plu
gin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser
```

ここまですることで、それっぽくディレクトリ構成ができてきました。
これでルートディレクトリに.prettierrcと.eslintrc.jsonを配備すれば最低限は完了です。

# 困った点

## create-react-appの仕様で./src/より外のフォルダを参照できない問題
create-react-appはデフォルトで./src配下のソースコードしか参照できません。しかし、モノレポを実現する際にfrontやdesktopから、どうしてもimport * from ../common/** みたいにしたいケースが出てきます。しかしこれではビルドエラーになってしまいます。ejectせずに、外のファルダを参照するためにreact-app-rewiredとカスタマイズ用のcustomize-craをインストールします。

```sh
$ ./packages/front/> yarn add -D react-app-rewired customize-cra
```

公式の手順に従い、package.jsonを書き換えます。
そして、設定上書き用のconfig-override.jsを作成します。

```js
const { removeModuleScopePlugin, override, babelInclude } = require("customize-cra");
const path = require("path");

module.exports = override(
  //remove limitaions of create-react-app
  removeModuleScopePlugin(),
  babelInclude([path.resolve("src"), path.resolve("../common/build")])
);
```

これで、ビルドしてみるとエラーが消えることが確認できると思います。

## create-react-appの仕様で、tsconfigのパス設定が消される問題　
モノレポ構成にしたことで、import * from ../common/** にするケースが出てきましたが、common側のフォルダが深くなるにつれて相対パスが地獄のような長さになってしまいます。
　そこでfrontフォルダ配下のtsconfig.jsonのpathsを設定します。

```
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@common/*":["../common/src/*"]
        }
    }
```

これで、

```
./packages/front$ yarn start
```

するとtsconfigのpaths設定がなぜか自動削除されます。自分の目を何度も疑いました（笑
create-react-app側で自動で上書きしてしまう仕様なそうで、Githubでは[回避策](https://github.com/facebook/create-react-app/issues/5645)が議論されています。
create-react-appが上書きするのはtsconfig.jsonだけなので、tsconfig.path.jsonを作成し、その中にパス設定を書きます。このtsconfig.path.jsonをextendsするtsconfig.jsonを作成すればいいのです。

```tsconfig.path.json
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@common/*":["../common/src/*"]
        }
    }
}
```

```tsconfig.json
{
  "extends": "./tsconfig.paths.json",
  "exclude": [
    "node_modules"
  ],
  "include": [
    "src"
  ],
  "compilerOptions": {}
}

```

さらにwebpackで名前解決するためにconfig-overrides.jsを修正します。addAliasの部分になります。

```js

const { removeModuleScopePlugin, override, babelInclude } = require("customize-cra");
const path = require("path");

function addAlias(config){
  config.resolve = {
    ...config.resolve,
    alias:{"@common":path.resolve(__dirname,"../common/build")}
  }
  return config
}

module.exports = override(
  addAlias,

  //remove limitaions of create-react-app
  removeModuleScopePlugin(),
  babelInclude([path.resolve("src"), path.resolve("../common/build")])
);
```

これにより、import文がimport * from @common/** と書けるようになり、無事に相対パス地獄から救われました。

## eslintの多重パッケージ問題

実際にプロトタイプでこの構成を試していく中で、yarn start時に下記エラーが発生しました。

```
There might be a problem with the project dependency tree.
It is likely not a bug in Create React App, but something you need to fix locally.

~~~~省略~~~~~
However, a different version of babel-eslint was detected higher up in the tree:
```

色々と調べていってみると、下記が問題であるとわかりました。
- create-react-appはeslintをインストールする
- desktop/frontのプロジェクトのcreate-react-appのバージョンが異なり、それぞれ異なるeslintが入っている
- プロジェクトルート直下に独立してeslintが入っている。

これで３個のeslintがプロジェクト直下のnode_modulesにインストールされていることがわかりました。悪夢…<br>yarn workspaceのnohoist機能を使うことも考えましたが、そもそもパッケージごとにreact-scriptsのバージョン違うのはどうなのよ？と思い、下記を実施しました。

desktop/frontプロジェクトのreact-scriptsのバージョンを揃えるプロジェクトルート直下のeslintを削除する

これで完璧ではないですが、プロジェクトで統一したeslintを使用することができます。理想を言えば、プロジェクトルート直下のpackage.jsonにeslintの情報が書いてあるべきなのですが…

# まとめ
yarn workspaceでtsconfig/eslint/prettierを共通化するモノレポ構成と構築方法について書いていきました。
今回使用したサンプルプロジェクトは[こちら](https://github.com/yohachiSuga/ts-monorepo)です。


参考
- https://qiita.com/suin/items/0128d3290820b0087357
- https://qiita.com/task-k/items/6286a2d9437cf6e182f0