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で分けておけばお互いがお互いのソースをなるべく触らずに実装を進めることができる。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark