最終更新: a few seconds agoRemove Netlify-related code from main (grafted, HEAD)

Lerna を使った Monorepo 化について

古い記述です

Lerna の動作の基本

Lerna の基本コンセプト的なところはここでは説明しないので Qiita あたりの紹介記事を参照。以下は技術的なメモ。

packages/ 配下のディレクトリに個々のパッケージが配置されている。基本的には、それぞれ package.json, package-lock.json を持った独立したパッケージであり、それぞれ NPM レジストリに publish することができる。

circus (monorepoルート)
  + lerna.json
  + package.json
  + node_modules (Lerna本体および共通devDeps)
  + packages
    + circus-api
      + package.json
      + node_modules
    + circus-web-ui
      + package.json
      + node_modules
    + circus-cs-core
      + package.json
      + node_modules
    + circus-rs
      + package.json
      + node_modules
    + circus-lib
      + package.json
      + node_modules
  • 一部の devDependencies (prettier, jest, eslint, etc) は個々のパッケージレベルではなく monorepo のルートにインストールされている。
  • それぞれのパッケージで共通で使われている dependencies は hoist(巻き上げ)され、<repo>/packages/pkg-a/node_modules ではなく <repo>/node_modules に配置されるようになる。これによりディスクスペースがちょっと減る。Node.js の require は目的のパッケージが見つかるまで node_modules を上に辿っていくのでこれで問題なく動く(ただし自分で書いた行儀の悪いプログラムなどでいくつか修正する必要がある)。
  • 依存している内部パッケージは symlink を使って結合される。例えば <repo>/packages/circus-api/node_modules/@utrad-ical/circus-lib<repo>/packages/circus-lib が symlink される。
  • 共通でない dependencies は普通に個別のパッケージディレクトリ内で <repo>/packages/circus-api/node_modules 配下にインストールされる。

注意点

相対パスでパッケージ間をまたがないこと!

個々のパッケージのルートを跨ぐ相対パスを使ったファイル参照は絶対に使わないこと。例えば <repo>/packages/circus-api/src 中から from '../../circus-lib/src' などとして import するのではなく、あくまで from '@utrad-ical/circus-lib/src' をインポートする。個々のパッケージは本来全く別の場所に配置されていているはずの独立したパッケージであるということを肝に銘じる。

dependencies と devDependencies を厳密に区別すること!

個々のパッケージで共通に使っている(dev のつかない) dependencies は、個々のリポジトリにそれぞれ宣言する必要がある(例えば multi-integer-range や fs-extra)。これらは publish してその個別パッケージだけ NPM install した時に、一緒に入らないと動作しない依存物たちなので当然。言い換えると、ルートの package.jsondependencies を書いてはいけない。厳密にバージョンを合わせる必要はないのだが hoist の効率が悪くなって警告は出るので、fs-extra みたいなもののバージョンは揃えておくのが望ましい。このためのコマンドとして lerna add があり、サブパッケージ間で dependencies のバージョンを合わせるのに使える。

一方で、個々のパッケージで共通に使っている devDependencies については、monorepo のルートにある package.json の devDependencies で宣言しても良い。ここにはツールチェイン系のもの(例えば prettier, typescript, eslint)などを入れることができ、ルートレベルで npm i eslint@latest, npx eslint などして使う。これらはリポジトリを GitHub からクローンしないとそもそも意味がないツールなので Lerna に任せて共通化するのが良い。

日々使う可能性がある最低限の作業レシピ

以下、特に明記されていない場合、コマンドは monorepo のルートディレクトリで発行する。

bootstrap

まず git clone したらやるのがこれ。リポジトリを pull した後にやるのもこれ。

npm ci
npx lerna bootstrap --hoist --ci

npm ci で、Lerna 自体や Jest など、ルート node_modules に入る共有 devDependencies を入れる。

npx lerna bootstrap --hoist --ci で、個々のパッケージの依存が入っていく。--hoist は共通パッケージの巻き上げを有効にし、--ci は(npm ci と同様に)package-lock.json の更新を防ぐ。これは個々のパッケージに対して npm install をし、相互のパッケージを symlink し、共通の dependencies を巻き上げ (hoist) する。

モジュールの追加や削除をする

個々のパッケージで必要な何かがある場合、普通に npm installnpm remove をすればいいが、その後に lerna bootstrap --hoist をしないと symlink が壊れるなどの副作用がある模様。

cd packages/circus-lib
npm install some-awesome-external-lib
cd ../..
lerna bootstrap --hoist

npm remove が失敗する場合、package.jsondependencies から該当モジュールを消した後に lerna bootstrap --hoist することでも消せる。

Lint, Prettier, Jest などを走らせる

これらは既に個々のパッケージの devDependencies に入っていないので、個々のパッケージ内npx jest などとしても動かない。これらは設定ごと共通化され、ルートの devDependencies としてインストールされている。以下はすべて monorepo のルートで作業する。

# 全パッケージに eslint をかける
npm run lint
npm run lint-fix

# 全パッケージで prettier をかける
npm run prettier

# 全パッケージで jest のテストを走らせる(以下のどちらでも同じ)
npm test
npx jest

# 個別に何かしたい場合はルートで npx する
npx eslint "packages/circus-lib/src/**/*.ts" --fix
npx prettier "packages/circus-api/src/**/*.ts" --write
npx jest circus-cs-core
npx jest StaticDicomFileRepository

個別パッケージそれぞれに対して何かする

lerna run を使うと個々のパッケージ内の NPM script を逐次実行できる。

lerna run build

対応するスクリプトが package.jsonscripts に存在しないパッケージでは、特に何もしない。

NPM script 以外のものを走らせる場合は lerna exec を使う。

node_modules 全部消す

個別モジュールの node_modules を全部消す。何か壊れた時やクリーンインストールできるか不安な場合に使う。

lerna clean --yes

ルートの node_modules は消されないので注意。そこは普通に npm ci で入れ直す。

ヒント

古い方のリポジトリ(utrad-ical/circus-api など)は早めに消すのがお勧め。「気づいたら古い方のリポジトリで作業していた」みたいな事故がいずれ起きそう。

ディレクトリが深くなりすぎて困る場合は以下の 2 つのどちらか(あるいは両方)を試すと良い。

  • ~/.bashrcCDPATH=/var/circus/circus/packages のような CDPATH 環境変数を設定して、cd circus-api だけで /var/circus/circus/packages/circus-api に飛べるようにする。
  • /var/circus/ 直下にシンボリックリンクを貼って今までと同じパスで移動できるようにする。