0

lernaでmonorepoした

monorepoという1つのリポジトリに複数プロジェクトを入れる開発手法がある。
この記事で知った。

単一リポジトリで複数package|projectを管理することをmonorepoというそう – なっく日報

そういえばRocketIOやnode-lindaを作っていた時は自作のライブラリ/パッケージが3,4層に積み上がっていて、単一パッケージ内で生じるバグはテストを書けば潰せるけどパッケージ間で起こるバグは非常に解決が難しかった。イベント発火のタイミングによるものとか。パッケージをまたがったテストをどこに書くのかという問題がある。

複数パッケージで起こったバグはどっちのissueに書けばいいのかわからないし、両方でブランチ切って同時に修正してリリースしたりとか超面倒だった。
バージョン毎の依存関係も、serverの1.3に対応してるのはclientの1.5で・・とか依存関係を書くのがややこしい。
テスト・ビルド用ツールも、それぞれのnpmに.babelrcとか.eslintrcとか置いて微妙に内容が違ったりすると混乱するし、統一したい。

と色々思う所があったのだが、1つのgitリポジトリに複数npmをまとめて入れて管理すればこの辺の悩みは解決しそうなのでmonorepoを試してみる事にした。


lerna

https://github.com/kittens/lerna

babelはmonorepoでやってる
Why is Babel a monorepo?

babel 6系になってからbabel-cliとかbabel-polyfillとかbabel-preset-es2015とかめちゃくちゃパッケージが分かれてて、でもバージョン番号は合っているのでこれどうやって管理してるんだ?と思ったらlernaというNode.jsでmonorepoするためのツールを使っていた。

コマンドはbootstrap、updated、publishの3つだけ。
リポジトリ内のpackage間の依存関係を解決したり、バージョン合わせながら一括npm publishしてくれたりする。
ドキュメントが無いのでソース読んで理解した

以下はlerna v1.1.0時点についてのメモ。


% lerna bootstrap

lib/commands/bootstrap.js
bootstrapすると必要なファイルが作られる。
packagesディレクトリが作られるので、その下に複数のnpmを置ける。
それぞれの中身は個別にnpm publishできるようにpackage.jsonや.npmignoreなどを置く(自分で)

rootディレクトリには開発用のpackage.jsonが作られ、devDependenceisにlernaがインストールされる。

rootにあるVERSIONというファイルにバージョン番号が書いてある。これはpublishに使われる。

また、ファイルが作られるだけでなくpackages下の各npmでnpm installが行われ(4並列でchild_process.execしている)、monorepo内で依存関係があればnpmjs.comからインストールせずにリンクが作られる。
module.exports = require("/Users/sho/src/nodejs/weather-yahoo-jp/packages/weather-yahoo-jp");
1行だけ書いてあるindex.jsが生成されて、リポジトリ内のローカルリンクになる。

ただしpackage.jsonのdependenciesに { “weather-yahoo-jp”: “^0.1.2” } のように^で始まるバージョン番号で指定していないとリンクを作ってくれない。
“*” や “0.1.2” のようなバージョン指定ではnpmjs.comからインストールしてしまうという罠がある。


bootstrapは新しくlernaを使っているリポジトリをgit cloneしてきた時に一気にnpm installするのにも使う。CIとか。
プルリクしたい人にもわかるように、「まずlerna bootstrapしろ」とREADMEに書いておくべき。


% lerna updated

lib/commands/updated.js
packages下の各npmについて、前回のpublishから更新があるか確認する



% lerna publish

lib/commands/publish.js
updatedなnpmを全てpublishする。
VERSIONファイルのパッチレベルが0.2.3→0.2.4のように1つ上がる。publishは対話式なので自分で番号入力したり、major,minor,patchでインクリメントもできる。

また、packagesの下の依存しあっているnpmのdependenciesで指定しているバージョン番号をVERSIONに置換してくれる。
これもbootstrapでのリンクと同じように、”^0.1.2″みたいな^が頭につく形式で書いてないと置換してくれない。

最後にgit tagを打って、remoteにgit pushしてくれる。


一回目は自分でset-upstreamしないとだめだった気がする
% git push --set-upstream origin master

release時にCHANGELOGなどを手書きしている場合は、git addだけしてcommitしていない状態でlerna publishすればcommitに含めてくれる。
特に変更が無くてupdatedに検知されていないpackages下のnpmも、package.jsonのdependenciesのバージョンをこれからpublishするバージョンに変更してgit addしておけばまとめてnpm publishとcommitしてくれる。


実際やってみる

weather-yahoo-jpという天気を取得するNodeライブラリと、CLIで天気を表示するツールを1つのリポジトリにまとめてmonorepo化してみた。

こういうディレクトリ構成になった
├── .babelrc # test/lint関係はrootにだけ置く
├── .eslintrc
├── .gitignore
├── README.md
├── VERSION # バージョン番号が書いてあるファイル
├── bin
│   └── run-each-packages # 全packagesで一気にコマンドを実行するツール(自作)
├── circle.yml # CircleCIの設定
├── package.json
└── packages # この下にnpmを複数置く
├── weather-yahoo-jp
│   ├── .npmignore # publish時に.es6ファイルなどを除外する
│   ├── History.txt
│   ├── README.md
│   ├── forecast-url.json
│   ├── package.json
│   ├── samples
│   │   ├── forecast.js
│   │   └── yolp.js
│   ├── src
│   │   ├── forecast.es6
│   │   ├── index.es6
│   │   ├── util.es6
│   │   └── yolp.es6
│   ├── test
│   │   ├── test_forecast.es6
│   │   ├── test_forecasturl.es6
│   │   ├── test_helper.es6
│   │   └── test_yolp.es6
│   └── tool
│   └── create-forecast-url-list.es6
└── weather-yahoo-jp-cli
├── .npmignore
├── README.md
├── bin
│   └── cli.js
├── package.json
├── src
│   ├── main.es6
│   └── util.es6
└── test
├── test_cli.es6
└── test_helper.es6
rootのpackage.jsonのdependenciesにビルド/テスト関係のnpmを全部入れて、各packages内はdevDependencies不要という構成にしたらすっきりした。ただしtestコードは各packagesの下に置く。
npmごとにbabelやeslint入れて微妙にバージョンずれて変な感じになったりしない。

testやlintの実行

rootから一気に実行すればいい
% eslint packages/*/*/*.es6
% mocha packages/*/test/*.es6 --compilers js:babel-register
test/lint関係のnpmはrootのpackage.jsonにインストールするようにした。


CI

CircleCIは一番上のディレクトリでのnpm installは自動的にやってくれる。
packagesの下のnpm installは自分でやらなければならないので、lerna bootstrapする。
ちなみにpackage.jsonのscriptsの中は./node_modules/.bin/にPATHが通っているのでnpm runで呼び出せばlernaをグローバルインストールしなくていい。


CircleCIではrootのnode_modulesしかcacheしないので、cache_directoriesにpackages/*/node_modulesを追加した。


各packages内でそれぞれコマンドを実行する

run-each-packagesというコマンドを作った。

% ./bin/run-each-packages babel src/ --out-dir lib/ # 全package内に移動してからbabel
% ./bin/run-each-packages --parallel babel src/ --out-dir lib/ --watch # 全packageでbabelをwatch
–parallelつけると並列実行する。
babelはrootディレクトリから実行すると書き出し先をpackages下のそれぞれに指定できなかったので、run-each-packagesが必要になった。

lerna boostrapがnpm installの経過を表示してくれなくて不安なので使ったりもする。
% ./bin/run-each-packages npm install
npm installはdependenciesが多すぎるとたまにエラー吐いて死ぬので経過を表示したい。


感想

lerna便利。
ディレクトリ構成も実装も処理内容もシンプルで理解しやすい。シンプルなのでかゆいところは自分で掻ける。

packages下のdependenciesを “^0.1.2” の形式で頭に^つけて書かなければローカルリンクしてくれない事以外はだいたい直感通りに動くので混乱なかった。

0

Edisonのnpmをupdateした

Edisonのnpmのバージョンが1.0系なのでnpm run test — –watchとかできなかったのでupdateした。

% npm update -g npm

したらnpmが壊れたので

module.js:340
throw err;
^
Error: Cannot find module 'npmlog'
at Function.Module._resolveFilename (module.js:338:15)
at Function.Module._load (module.js:280:25)
at Module.require (module.js:364:17)
at require (module.js:380:17)
at /usr/lib/node_modules/npm/bin/npm-cli.js:18:11
at Object. (/usr/lib/node_modules/npm/bin/npm-cli.js:75:3)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)


ソースからインストールしなおした。

% opkg install tar
% curl -0 -L https://www.npmjs.com/install.sh | sh
% npm --version
2.11.2

0

Node用rocketio-clientを作ったのと、npmの作り方

Sinatra::RocketIOのNode.js用クライアントを作った、という事を書くのを忘れていた。
自作Nodeモジュールをnpmjs.orgへ登録する方法もまとめる。


https://github.com/shokai/node-rocketio-client
https://npmjs.org/package/rocketio-client


感想

npmに初めて登録したけど、rubygems.orgの良い所はそのままにより洗練されてる感じがした。
あとcoffee-script、昔触った時は何これすっげえキモイと思って無理だったんだけど、その頃よりも構文が充実してたし、
Scalaを少し勉強したお陰で目が慣れてて普通に書けるようになってた。

Rubyとscala書ける人はcoffee-scriptすんなり入れると思う。


インストール

npm install rocketio-client

使う


ごく普通にSinatra::RocketIOに接続できる。websocketとcometの使える方が選択される。
RocketIO = require 'rocketio-client'
io = new RocketIO('http://localhost:5000').connect()

io.on 'connect', (io)->
console.log "connect!! (#{io.type})"
io.push 'hello', 'hello world'

io.on 'echo', (data)->
console.log "echo> #{data}"


サーバー側はこんな感じ
main.rb
io = Sinatra::RocketIO

## receive "hello" from client
io.on :hello do |message, client|
puts "> receive '#{message}' from #{client.session} (#{client.type} #{client.address})"

## push "echo" to client
io.push :echo, message
end


自作nodeモジュールをnpmjs.orgに登録する方法

package.jsonを作る

% npm init
対話形式でライブラリの名前やauthor、ライセンス形態を質問されるので答えるとpackage.jsonが生成される

package.jsonを直接編集して、必要あれば項目を追加する。
  • “dependencies” に実行時の依存ライブラリを書く
  • “devDependencies” に開発時に必要なライブラリを書く
  • 実行可能コマンドも配布する場合、 “bin” にpathを書く
  • “main” で指定されたファイルが、require ‘パッケージ名’ された時に読み込まれるファイルになる

もう一度npm initすれば対話形式で再編集できる。


ディレクトリ構成

.
├── History.txt
├── Makefile
├── README.md
├── lib
│   └── rocketio-client.js
├── node_modules

│  (node_modules内は多すぎなので省略)

├── sample
│   └── sample.coffee
├── server
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── Procfile
│   ├── README.md
│   ├── config.ru
│   ├── lib
│   ├── main.rb
│   ├── npm-debug.log
│   ├── public
│   │   ├── index.js
│   │   └── jquery.min.js
│   └── views
│   └── index.erb
└── src
   └── rocketio-client.coffee


プログラムを書く

他の人のnpmを見ていると、大抵libディレクトリの下にjsを置くようだ。

coffee-scriptを使ってる場合は、lib以外のディレクトリ(src等)を作ってcoffeeはそこに置いて、
都度jsにコンパイルしてlibに書き出す。
Makefileでやった。

サンプルコードを書く

requireはモジュール名やファイル名ではなくディレクトリ名を指定した場合に、そこにpackage.jsonがあれば”main”の項目に指定されたファイルを読み込んでくれる。

なのでsample/sample.coffeeからrocketio-clientをrequireするには
RocketIO = require '../'
と相対パスで書けばいい。


README.mdを書く

markdownでREADME.mdを書くとnpmjs.orgで綺麗に表示してくれる


publish

% npm publish
npmの開発者アカウントを作っていない場合は「adduserしろ」等の指示が表示される。言われたとおりにやる。

% npm adduser
指示に従って入力していけばok

これでもう公開されているので、誰でもインストールしてすぐ使える。


開発中のnpmを公開前にローカルで試す方法

package.jsonのdependenciesにはgitリポジトリを指定できるので、それでインストールできる。

もしくは普通nodeで開発している時はプロジェクトルートにnode_modulesというディレクトリができてその中にnpmがインストールされるので、シンボリックリンクでローカルの開発中のnpmに差し替えるという手もあると思う。