Create React App(CRA) + Lernaでモノレポ管理
「そこまでするか」という気もしないではないですが、やってみてから考えようと思いまして。 やりたいこと CRA本体のテスト・コード依存を減らしたい TypeScriptに段階的マイグレしたい でもリポジトリわけるのはアプ […]
目次
「そこまでするか」という気もしないではないですが、やってみてから考えようと思いまして。
やりたいこと
- CRA本体のテスト・コード依存を減らしたい
- TypeScriptに段階的マイグレしたい
- でもリポジトリわけるのはアプデとか諸々面倒な予感しかしない
仮説
- Lernaでモノレポにする
- たとえばViewと処理系(非同期・同期)でリポジトリをわける
- 同期処理系 -> View -> 非同期処理系みたいに段階的にTypeScriptにする
- 汎用系があれば、それだけNPMに公開したら他のサービスにも応用できそう
検証
ということで早速試してみる
目指す形
ひとまず「アプリ本体」「処理部分」「View」の3つに分けてみます。
.
├── lerna.json
├── package.json
└── packages
├── my-app
├── my-app-functions
└── my-app-components
プロジェクトのセットアップ
まずはLernaでプロジェクトをセットアップします。
$ mkdir lerna-react && cd lerna-react
$ lerna --version
3.13.2
$ lerna init
lerna notice cli v3.13.2
lerna info Initializing Git repository
lerna info Creating package.json
lerna info Creating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files
Lernaで管理されるものは、デフォルトではpackages/
配下に配置されます。
React Appのセットアップ
続いてReact Appを立ち上げます。今回はCreate React Appを使います。
$ cd packages/
$ npx create-react-app my-app --typescript
[OPT] package.jsonの書き換え
lernaプロジェクトのルートにあるpackage.jsonでインストールしておけば、全てのリポジトリでインポートできます。ただしCLIについては各リポジトリのルートで入れる必要がありますので、以下のようにそれぞれpackage.jsonを書き換えます。
ただしあとで使用するnwbなど、React Componentを作る系のライブラリを使用するのであれば特にこのステップをやる必要はありません。そちらがreact / react-domをリポジトリにインストールしてくるので、ルートのものをよんでくれなくなります。
./package.json
{
"name": "root",
"private": true,
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"devDependencies": {
"lerna": "^3.13.2"
}
}
./packages/my-app/package.json
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"react-scripts": "2.1.8"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
やったこと
react / react-domをルートに移動し、CLIで利用するreact-scriptsのみ残しました。最後に設定どおりにライブラリをインストールし直しましょう。
$ lerna clean && npm install && lerna bootstrap
React Appを起動させてみる
ここまでくれば、lernaでReat Appを起動できます。以下のようにコマンドを打ちましょう。
$ lerna run --scope my-app start
TypeScriptのセットアップ
やらなくても別に問題ないですが、せっかくなのでこれもトライしてみます。
$ tree -L 2
.
├── lerna.json
├── package.json
└── packages
└── my-app
2 directories, 2 files
// 型情報は共有ライブラリ扱いにする
$ npm i -D @types/node @types/react @types/react-dom @types/jest
// tscコマンドを使うので、こちらはパッケージに入れる
$ lerna add typescript -D
これで準備OKです。tsc --init
はCreate React App側がやってくれるので、以下のようにコマンドを打てばApp.js
をTypeScriptに切り替えできます。
$ mv packages/my-app/src/App.js packages/my-app/src/App.tsx
$ lerna run --scope my-app start
ちなみに以下の2ファイルが作られます。
packages/my-app/src/react-app-env.d.ts
packages/my-app/tsconfig.json
処理系パッケージを追加する
続いて処理系をまとめるパッケージを追加します。
パッケージの追加はlerna create
を使います。動きとしてはmkdir <path> && npm init
に近く、-y
をつけるとデフォルト設定が適用されます。細かく設定したい方は-y
を外して対話式で設定しましょう。
$ lerna create my-app-functions -y
追加されたファイルの構造は以下のとおりです。
$ tree -L 2
.
├── README.md
├── __tests__
│ └── my-app-functions.test.js
├── lib
│ └── my-app-functions.js
└── package.json
Jestを追加する
テスト用のファイルが用意されていますが、ツールが入っていませんのでテストできません。ということでJestを追加します。
$ lerna add --scope my-app-functions -D jest
package.jsonにjestコマンドを入れてやることで、このパッケージ内でJestのテストができるようになります。
$ vim packages/my-app-functions/package.json
...
"scripts": {
"test": "jest"
},
...
簡単なテストを用意する
シンプルなテストコードを追加しておきましょう。
$ vim packages/my-app-functions/__tests__/my-app-functions.test.js
describe('my-app-functions', () => {
it('first test', () => expect(true).toEqual(true))
});
テストを実行する
続いてテストを実行します。
$ lerna run --scope my-app-functions test
> jest
lerna success run Ran npm script 'test' in 1 package in 3.5s:
lerna success - my-app-functions
成功しました。
TypeScriptを追加する
続いてこちらにもTypeScriptをいれます。
$ lerna add --scope my-app-functions -D typescript
全部のリポジトリに入れたい場合は、--scope my-app-functions
をはずしてやればOKです。
tsconfig.json
の設定をnpxでやるには、各リポジトリまで移動してコマンドを打つ必要があります。それはめんどくさいので、package.json
にnpm-scriptを追加して、そちらから実行しましょう。
$ vim packages/my-app-functions/package.json
...
"scripts": {
"test": "jest",
"build": "tsc"
},
...
$ lerna run --scope my-app-functions build -- -- --init
lerna -> npm -> tscのバケツリレーが発生する関係で、--
がちょっと多めです。
tsconfig.jsonは以下のような形にしました。
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"outDir": "./dist",
"rootDir": "./lib",
"strict": true,
"declaration": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true
},
"exclude": [
"__tests__",
"dist/"
]
}
TypeScriptでコード・テストを書く
続いて動かす処理とテストを書きます。
処理
$ mv packages/my-app-functions/lib/my-app-functions.js packages/my-app-functions/lib/my-app-functions.ts
$ vim packages/my-app-functions/lib/my-app-functions.ts
export const myAppFunctions = (name: string): string => {
return `Hello ${name}`
}
テスト
$ mv packages/my-app-functions/__tests__/my-app-functions.test.js packages/my-app-functions/__tests__/my-app-functions.test.ts
$ vim packages/my-app-functions/__tests__/my-app-functions.test.ts
import { myAppFunctions } from '../lib/my-app-functions';
describe('my-app-functions', () => {
it('needs tests', () => {
expect(myAppFunctions('john')).toEqual('Hello john')
});
});
package.json
$ vim packages/my-app-functions/package.json
...
"main": "dist/my-app-functions.js",
ビルドコマンド
ビルドもlerna経由で実行可能です。成功すると、distディレクトリが生成されます。
$ lerna run --scope my-app-functions build
$ tree packages/my-app-functions/ -I node_modules
packages/my-app-functions/
├── README.md
├── __tests__
│ └── my-app-functions.test.ts
├── dist
│ └── my-app-functions.js
├── lib
│ └── my-app-functions.ts
├── package-lock.json
├── package.json
└── tsconfig.json
3 directories, 7 files
作成した処理をReactから呼び出す
これで処理系のリポジトリができたので、React側からよびましょう。
lerna addコマンドで追加できます。
$ lerna add my-app-functions --scope=my-app
後はアプリ側から関数をよびだしてやればOKです。
$ vim packages/my-app/src/App.tsx
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import * as test from 'my-app-functions'
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1>{test.myAppFunctions('John')}</h1>
...
処理系リポジトリを更新する
せっかくなので、更新してみましょう。
$ vim packages/my-app-functions/lib/my-app-functions.ts
export const myAppFunctions = (name: string): string => {
return `Hora ${name}`
}
$ lerna run --scope=my-app-functions build
buildコマンドが終わり次第、React側も更新されます。
Componentリポジトリを追加する
続いて共有Componentを管理するリポジトリを作ります。今回はnwbを使いました。
// ディレクトリへ移動
$ cd packages
// nwbでコンポーネントライブラリのリポジトリを作る
$ nwb new react-component my-app-components
// ルートに戻る
$ cd ../
// npm startをlernaから実行
$ lerna exec npm start --scope my-app-components
これでpackages/my-app-components
配下にあるnwbで作成されたコンポーネントを作っていくことができます。
なお、nwbはデフォルトで.git
ディレクトリを作るので、モノレポ管理するなら明示的に消してやりましょう。
$ rm -rf packages/my-app-components/.git
$ git add packages/my-app-components
$ git commit -m "add component package made by nwb"
nwbでコンポーネントを用意する
簡単なコンポーネントを作っておきましょう。
packages/my-app-components/src/index.js
import React, {Component} from 'react'
export default ({header, title, children, footer}) => (
<article>
<header>
<h1>{header}</h1>
</header>
<main>
<h2>{title}</h2>
{children}
</main>
<footer>{footer}</footer>
</article>
)
my-appから読み込む
あとはfunctionと同様にlerna add した後に読み込みしてやればOKです。
$ lerna add --scope=my-app my-app-components
Bootstrapをいれる
UIライブラリを共通で入れる場合、やはりrootに入れるのがよいでしょう。
$ npm install -S bootstrap reactstrap
というのもCSSを別途importする必要がある場合があり、その場合はcomponentリポジトリの表示テスト用のファイルとapp側のファイルそれぞれでimportする必要があるからです。
Demo用: packages/my-app-components/demo/src/index.js
import React, {Component} from 'react'
import {render} from 'react-dom'
import 'bootstrap/dist/css/bootstrap.min.css';
import Example from '../../src'
class Demo extends Component {
render() {
return <div>
<h1>my-app-components Demo</h1>
<Example header="test" title="Hi!">
Hello wolrd
</Example>
</div>
}
}
render(<Demo/>, document.querySelector('#demo'))
App用: packages/my-app/src/App.jsx
import React, { Component } from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
const Example = require('my-app-components/lib/index')
class App extends Component {
render() {
return (
<div className="App">
<Example header="test" title="Hi!">
Hello
</Example>
</div>
);
}
}
export default App;
トラブった時
理由がわかっていないのですが、たまにlerna add
が404エラーでコケます。npmjsを見に行ってしまってるっぽいのかな。。
とりあえずそんなときは以下のようにして入れ直してやると解決しました。
$ lerna clean
$ lerna bootstrap
感想
便利といえば便利。ただしCircle CI / Travis CIあたりで自動ビルド・デプロイを組もうと思うと結構つらそうな予感がする。
特に以下のようなケースで遅延がつらくなりような気配がある。
- componentsはnpmに公開する
- functionsはprivateのまま、TypeScriptのビルドだけ行う
- appはprivateでNetlifyにビルドアンドデプロイ
1Gitリポジトリになるので、「componentsだけ出す」とかをやりだすと結局各ディレクトリに入ってnpm publishするの?という気持ちもある。
なんとなくこの構成が向いていそうなケース
自社の複数サービスで、View・処理系を共通化したい
大体の場合主となるサービスがあって、それ以外のサービスのView / 処理系を主となるサービスに揃えたいという要件になる。なので
- 主となるサービスをlernaでmonorepoにして、そこで開発を進行
- 共通化するview / 処理系は他のサービスに反映させるタイミングでnpm publish
- 従となるサービス側でパッケージをアップデートする
という流れにすると、主となるサービスの開発環境でわりとガンガンソースを書いていくことができる。
ただしnpm installでgitリポジトリを参照するやり方がほぼ使えなくなりそうという問題もある。というのも主となるサービスのダッシュボードのソースまでDLしてしまうため、webpackなどでtree shakingしてやらないとファイルサイズが楽しいことになる恐れがあるため。
Viewを見る人と処理を見る人がほぼ完全に別人
ViewだけUI / UXよりの人が実装をすすめて、API / 処理系統を別の人がみるという進め方もFlux的な設計にすると可能になる。この場合Viewの部分をmonorepoで分けておけばお互いがお互いのソースをなるべく触らずに実装を進めることができる。