npm workspace で始めるモノレポ生活

workspace とは npmv7 から追加された機能で、 yarnpnpm の workspace を追従した機能です。

workspace は複数のパッケージ(package.json)をレポジトリを管理するために使用されます。このようなレポジトリはモノレポとして知られています。例えば以下のようにバックエンドとフロントエンドのプロジェクトをただ一つのレポジトリで管理するようにします。

.
├── backend
│   ├── src
│   │   ├── server.ts
│   │   └── ...
│   ├── package.json
│   └── package-lock.json
└── frontend
    ├── src
    │   ├── main.ts
    │   ├── components
    │   └── ...
    ├── package.json
    └── package-lock.json

モノレポの戦略を選択した場合全てのチームが同じレポジトリを共有することになります。一般にモノレポを選択する理由として以下のようなメリットがあげられます。

  • コードの再利用
  • チーム間のコラボレーションが容易になる
  • コードの結合性
  • コードの全体を把握しやすい
  • 大規模なリファクタリングがやりやすい
  • リリースを管理しやすい

もちろんモノレポを選択した場合にもたらされるのはメリットだけではなく、いくつかのデメリットと複雑性も生じます。例えば

  • 各プロジェクトのパッケージの依存関係の管理
  • CI のジョブの複雑化

npm workspace は1つ目の「各プロジェクトのパッケージの依存関係の管理」を解決するために用いられます。npm workspace は複数のプロジェクトのパッケージをトップレベルの package-json で管理することができます。また似た機能を提供するツールに Lerna が存在します。

例えば、バックエンドとフロントエンドのプロジェクトどちらも Jest パッケージを使用しているという例を考えてます。

npm workspace を始める

それでは実際に npm workspace を始めてみましょう。npm workspace を利用するには npm のバージョン7以降が必要です。

$ npm install -g npm@latest
$ npm -v
8.6.0

ルートパッケージを作成する

まずはプロジェクトのトップレベルで npm プロジェクトを作成します。

$ npm init -y

このパッケージは公開することはないので private フィールドを true にしておくとよいです。

  {
+   "private": true
  }

ワークスペースを作成する

ルートパッケージ上で npm init コマンドに -w オプションを付与することでワークスペースを作成できます。-w オプションの引数にディレクトリ名を指定します。

$ npm init -w backend

上記コマンドを実行すると backend ディレクトリが作成されその中に package.json ファイルが作成されます。さらにルートパッケージの package.json ファイルには workspace フィールドが追加されます。

  {
 +  "workspaces": [
 +    "backend"
 +  ]
  }

続いてフロントエンドのパッケージも追加しておきましょう。

$ npm init -w frontend

現在のレポジトリの構成は以下のようになっています。

.
├── package.json
├── frontend
│   └── package.json
└── backend
    └── package.json

パッケージをインストールする

npm install コマンドでパッケージをインストールする際に -ws オプションを付与することで全てのワークスペースに対して単一のパッケージをインストールすることができます。

$ npm install lodash -ws

この時、レポジトリの構造は以下のようになります。

.
├── backend
│   ├── package.json
├── frontend
│   └── package.json
├── node_modules
│   ├── backend -> ../backend  
│   ├── frontend -> ../frontend
│   └── lodash
├── package-lock.json
└── package.json

ルートパッケージの node_moduelslodash が配置されるので npm のホイスティング(巻き上げ)により、よりそれぞれのワークスペースで lodash をインストールせずとも使用することができます。

// frontend/index.js
const lodash = require("lodash");

const chunked = lodash.chunk(["a", "b", "c", "d"], 2);

console.log(chunked);
$ node frontend/index.js 
[ [ 'a', 'b' ], [ 'c', 'd' ] ]

特定のワークスペースのみにパッケージをインストールしたい場合には -w オプションで対象のワークスペースを指定してインストールコマンドを実行します。

$ npm i dayjs -w backend

フロントエンド、バックエンドのそれぞれの dependencies は次のようになっています。

// frontend/package.json
{
  "name": "frontend",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}
// frontend/package.json
{
  "name": "backend",
  "dependencies": {
    "dayjs": "^1.11.0",
    "lodash": "^4.17.21"
  }
}

npm スクリプトの実行

npm スクリプトを実行する場合にもパッケージをインストールする場合と同様にワークスペースを指定することができます。例えば次のコマンドは全てのワークスペースをテストを実行します。

$ npm run test -ws

以下のコマンドはフロントエンドのテストのみを実行します。

$ npm run test -w frontend

また、-ws オプションで全てのワークスペースの npm スクリプトを実行する際にワークスペースを特定のワークスペースのみに対象の npm スクリプトが定義されていないかもしれません。例えば serve コマンドはバックエンドプロジェクトには定義されていますがフロントエンドのプロジェクトには定義されていない例を考えてます。

// backend/package.json
{
  "name": "backend",
  "scripts": {
    "test": "jest",
    "serve": "node index.js"
  },
}
// frontend/package.json
{
  "name": "fronend",
  "scripts": {
    "test": "jest"
  },
}

このような状態でスクリプトを実行するとエラーが発生してしまいます。

$ npm run serve -ws 

> backend@1.0.0 serve
> node index.js

server
npm ERR! Lifecycle script `serve` failed with error: 
npm ERR! Error: Missing script: "serve"

To see a list of scripts, run:
  npm run 
npm ERR!   in workspace: frontend@1.0.0 
npm ERR!   at location: /npm-workspace/frontend 

このような場合には --if-present オプションを付与して実行すると npm スクリプトが定義されているワークスペースのみが実行されます。

$ npm run serve -ws --if-present

> backend@1.0.0 serve
> node index.js

server

さらに全てのワークスペースの npm スクリプトを実行する時実行順序はルートパッケージの workspaces フィールドの順番に依存されます。例えば workspaces の順場が backendfrontend となっている場合

{
  "workspaces": [
    "backend",
    "frontend"
  ]
}

実行結果は以下のようになります。

$ npm run test -ws

> backend@1.0.0 test
> echo I am backend

I am backend

> frontend@1.0.0 test
> echo I am frontend

I am frontend

次にルートパッケージの workspaces の順序を入れ替えて実行しています。

{
  "workspaces": [
    "frontend",
    "backend"
  ]
}

```sh
$ npm run test -ws

> backend@1.0.0 test
> echo I am backend

I am backend

> frontend@1.0.0 test
> echo I am frontend

I am frontend

今度は frontendbackend の順番で実行されます。

$ npm run test -ws

> frontend@1.0.0 test
> echo I am frontend

I am frontend

> backend@1.0.0 test
> echo I am backend

I am backend

ワークスペース間でコードを共有する

npm workspaces でワークスペースを管理している場合それぞれのワークスペース同士でパッケージを参照することができます。例えば、次のようにバックエンドで作成したコードをフロントエンドから参照することができます。

// backend/utils.js
const add = (a, b) => a + b;

module.exports = {
  add,
};
const utils = require("backend/utils");

const result = utils.add(1, 2);

console.log(result);
$ node frontend/index.js
3

この記事をシェアする
Hatena

関連記事