相互依存関係(Sibling packages)にあるJavaScriptのマルチプロジェクト管理を lerna でやってみる

## はじめに

たとえば、下記のような相互依存関係のあるプロジェクト群をJavaScriptで作りたい場合が普通にあるかと思います。

```
start-lerna/
  packages/
    common/ (Moment.jsに依存)
    childA/ (../common に依存)
    childB/ (../common, lodash に依存)
```

本記事で、このようなプロジェクトを [lerna](https://github.com/lerna/lerna/) を使って構築・管理してみます。あまり高度な使い方には触れません。というか、私自身も初心者なので、触れられません。 (^^;)

なお、パッケージ管理に [yarn](https://yarnpkg.com/lang/ja/) を使います。別に使わなくてもできます。

この記事で使用しているソースコードは https://github.com/knjname/2018-07_start-lerna にて利用可能です。

## lerna プロジェクト（親プロジェクト） を作る

```console
# プロジェクトフォルダを作る
$ mkdir start-lerna
$ cd start-lerna

# lernaの初期化
$ yarn add -D lerna
$ yarn lerna init
...
lerna success Initialized Lerna files

# 中身の確認 (勝手に　.git フォルダも作られている)
$ ls -a
.git
lerna.json
package.json
packages
```

パッケージ管理にyarn を使うので、 `lerna.json` の `npmClient` キーに `"yarn"` を指定しましょう。

```json:lerna.json
  "npmClient": "yarn",
```

## 子プロジェクトを作る

lerna のルールとしては、 `親プロジェクト/packages/子プロジェクトたち` という場所に子プロジェクトを置くのがデフォルトなので、それに反しないように作っていきます。

### `common` プロジェクト

```console:PWD=プロジェクトルート
$ cd packages

$ mkdir common
$ cd common

# 下記コマンドで version=1.0.0, main: index.js な package.json ができるはず
$ yarn init

# Moment.js への依存性追加
$ yarn add moment
```

```javascript:packages/common/index.js
const moment = require("moment");

module.exports = function (name) {
    return "Hello, " + name + ". I am common module you're depending on. And it's " + moment(new Date()).format("h:mm") + " now.";
}
```

### `childA` プロジェクト (兄弟プロジェクトへの依存性を持っているプロジェクト)

```console:PWD=プロジェクトルート
$ cd packages

$ mkdir childA
$ cd childA
$ yarn init
```

手作業で `package.json` に `common` への依存性を追加しましょう。

```json:packages/childA/package.json
  "dependencies": {
    "common": "^1.0.0"
  }
```

`common` のモジュールを使ったスクリプトも追加しましょう。

```javascript:packages/childA/index.js
const common = require("common");

console.log(common("knjname"));
```

上記の `common` への依存性がある（そして推移的に Moment.js に依存性がある） `childA` を実際に動かしてみましょう。

```console:PWD=プロジェクトルート
# 子プロジェクトの依存性解決
$ yarn lerna bootstrap

$ cd packages/childA

$ node index.js
Hello, knjname. I am common module you're depending on. And it's 8:41 now.
```

ちゃんと `common` への依存性も、 `common` 自体の依存性(= `moment`) も解決した状態で実行されましたね。

ちょっと `common/index.js` の内容を変更してみましょう。

```javascript:packages/common/index.js
const moment = require("moment");

module.exports = function (name) {
    return "Good bye, " + name + ". I am common module you had depended on. And it was " + moment(new Date()).format("h:mm") + " now.";
}
```

そうすると、特に何もしなくても変更に追従してくれますね。

```console:PWD=packages/childA
$ node index.js
Hello, knjname. I am common module you're depending on. And it's 8:41 now.
```

`packages/childA/node_modules/common` が `../../common` へのシンボリックリンクになっているからです。

```console
$ ls -al packages/childA/node_modules
common -> ../../common
```

### `childB` プロジェクト (兄弟プロジェクトと `lodash` への依存性を持っているプロジェクト)

今度は兄弟プロジェクトへも、独自 `lodash` にも依存性を持っているプロジェクトを作成してみます。

```console:PWD=プロジェクトルート
$ cd packages

$ mkdir childB
$ cd childB
$ yarn init
```

`common` への依存性をまず足そうと思うのですが、前回のように手で `package.json` に追加することはせず、 `lerna` コマンドでやってみます。

```console:PWD=プロジェクトルート
$ yarn lerna add common --scope=childB
...
lerna success Bootstrapped 1 packages
✨  Done in 0.84s.
```

このコマンドで `childB/package.json` に依存性エントリが追加されます。

```json:packages/childB/package.json
  "dependencies": {
    "common": "^1.0.0"
  }
```

更に `lodash` への依存性も追加しましょう。

```console:PWD=プロジェクトルート
$ yarn lerna add lodash --scope=childB
 :
 :
lerna success Bootstrapped 1 packages
✨  Done in 0.84s.
```

同じように `childB/package.json` に依存性エントリが追加されるのですが、 `childB/yarn.lock` ファイルも自動生成されます。ありがたい。

```json:packages/childB/package.json
  "dependencies": {
    "common": "^1.0.0",
    "lodash": "^4.17.10"
  }
```
```text:packages/childB/yarn.lock
lodash@^4.17.10:
  version "4.17.10"
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
```

もちろんこれら依存関係を用いたスクリプトを作らないと意味がありませんね。

```javascript:packages/childB/index.js
const common = require("common");
const _ = require("lodash")

console.log(_.map(["foo", "bar", "baz"], function (elem) {
    return common(elem)
}))
```

```console:PWD=packages/childB
$ node index.js
[ 'Good bye, foo. I am common module you had depended on. And it was 8:59 now.',
  'Good bye, bar. I am common module you had depended on. And it was 8:59 now.',
  'Good bye, baz. I am common module you had depended on. And it was 8:59 now.' ]
```

ちゃんと動きました！

## プロジェクトを初期からインストールさせてみる

上記で簡単なプロジェクト構成を作成しましたが、当然 `node_modules` などはバージョン管理には入れないので、他人がプロジェクトをチェックアウトした際に数コマンドでセットアップできないと困ったことになります。

きちんとできるか、検証してみましょう。

```console:PWD=プロジェクトルート
# まずは node_modules を全消ししてみる
$ rm -rf node_modules packages/*/node_modules

# lerna 自体（親プロジェクト自体）のインストール
$ yarn

# lerna による全プロジェクトの依存性インストール
$ yarn lerna bootstrap
...
lerna success Bootstrapped 3 packages
✨  Done in 2.21s.
```

ちゃんとできました！

![image.png](https://qiita-image-store.s3.amazonaws.com/0/9619/0ba8e706-bd51-0c60-578e-0087f8ad50ce.png)

## おわりに

以上のやり方で小規模なプロジェクトは運用できると思います。たとえば、ソースコード生成系のプロジェクトを lerna を使って分離するなどの手法を取ることができます。

もっと高度な使い方は [公式ドキュメント](https://github.com/lerna/lerna) を見ましょう。