30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Yarn のワークスペースの初歩

Yarn にはワークスペースという機能があり、これを使うとひとつのリポジトリで複数の npm パッケージを開発できるようになり、効率的に作業を進められるようになる。

この記事の内容は、Yarn のv1.22.4で動作確認している。

シンボリックリンクが作られ、パッケージをまたいだ開発をしやすくなる

ワークスペースをつかうためには、リポジトリのルートディレクトリにpackage.jsonを作成し、workspacesフィールドを指定する必要がある。
また、このディレクトリをパッケージとして公開することはないので、privateフィールドをtrueにしておく。

{
  "private": true,
  "workspaces": ["project-a"]
}

上記の例では、project-aというディレクトリを、ワークスペースの対象としている。
次に、以下の内容の./project-a/package.jsonを作成する。

{
  "name": "project-a",
  "version": "1.0.0"
}

現在のディレクトリ構成は以下のようになっている。

├── package.json
└── project-a
    └── package.json

この状態で、ルートディレクトリで$ yarnを実行する。
すると、./yarn.lock./node_modulesが作られる。

注目すべきは./node_modulesで、そのなかにproject-aというファイルが作られ、それは./project-aへのシンボリックリンクになっている。

$ ls -l node_modules/
total 0
lrwxr-xr-x  1 numb  staff  12  7 20 17:32 project-a -> ../project-a

$ cat node_modules/project-a/package.json
{
  "name": "project-a",
  "version": "1.0.0"
}

これだけだと何がありがたいのかイメージしづらいが、このリポジトリ内の他のプロジェクトがproject-aに依存しているときに、便利になる。

早速、project-aに依存するproject-bを作っていく。

まず、./project-a/foo.jsを作る。

module.exports = {
  x: 1,
}

そして、./project-a/package.jsonmainフィールドにfoo.jsを指定する。

{
  "name": "project-a",
  "version": "1.0.0",
  "main": "./foo.js"
}

これで、project-aを npm パッケージとして公開した時は、require('project-a')foo.jsが読み込まれるようになる。

次に、以下の内容の./project-b/package.json./project-b/bar.jsを作る。

{
  "name": "project-b",
  "version": "1.0.0",
  "main": "./bar.js",
  "dependencies": {
    "project-a": "1.0.0"
  }
}
const {x} = require('project-a');

console.log(x);

dependenciesに書いたように、project-bproject-aに依存しており、それを読み込んでいる。
だが、project-aはまだ npm パッケージとして公開していない。
この状況で./project-b/bar.jsを実行するとどうなるか。

$ node project-b/bar.js
1

問題なく動作する。
なぜなら./node_modulesにはproject-aが存在しており、それは./project-aへのシンボリックリンクになっているためである。

この仕組みによって、パッケージをまたいだ修正や変更がやりやすくなる。

例えば、project-aを公開したあとに、xではなくyという変数をエクスポートすべきであったことに気付き、修正することになったとする。
そうすると、その修正の影響はproject-bにも及ぶ。
こういったケースでの動作確認を、ワークスペースの仕組みによって非常に簡単に行える。

まず、./project-a/foo.jsを修正する。

module.exports = {
  y: 1,
}

そうすると、その変更は自動的にproject-bにも反映される。

$ node project-b/bar.js
undefined

xではなくyを読み込めばよいはずなので、./project-b/bar.jsを修正する。

const {y} = require('project-a');

console.log(y);

無事に動くことを確認できた。

$ node project-b/bar.js
1

このように、手元の環境での動作確認が非常に簡単になる。

ルートディレクトリの yarn.lock で依存パッケージを一元管理できる

ワークスペースを使うと、依存パッケージの管理も簡便になる。
パッケージ毎にロックファイルを作成するのではなく、ルートディレクトリに用意したひとつのロックファイルで管理していくことになる。

まず、./package.jsonを更新して、project-bもワークスペースの対象にする。

{
  "private": true,
  "workspaces": [
    "project-a",
    "project-b"
  ]
}

そしてそれぞれのプロジェクトの依存関係に、currency.js@2.0.0を加える。

{
  "name": "project-a",
  "version": "1.0.0",
  "main": "./foo.js",
  "dependencies": {
    "currency.js": "2.0.0"
  }
}
{
  "name": "project-b",
  "version": "1.0.0",
  "main": "./bar.js",
  "dependencies": {
    "project-a": "1.0.0",
    "currency.js": "2.0.0"
  }
}

この状態で、ルートディレクトリで$ yarnを実行する。
そうすると、ルートディレクトリのyarn.lockに以下のように記述される。

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


currency.js@2.0.0:
  version "2.0.0"
  resolved "https://registry.yarnpkg.com/currency.js/-/currency.js-2.0.0.tgz#f23b5d8b963519e419ee051103fa44168eb5a8f1"
  integrity sha512-W1OqppUNYbxcTVKXI4oV9dkBlg/GWhYpEbvKzDl79JeljAEI6KNdjreX66/s59C7Fg+MjDwhV6RbT/gLVZJg9A==

そして、node_modulesも各パッケージ毎に作成されるのではなく、ルートディレクトリにあるnode_modulesに、currency.jsがインストールされる。

$ ls node_modules/
currency.js project-a   project-b

そしてもちろん、問題なく読み込める。

// ./project-b/bar.js
const currency = require('currency.js');
const {y} = require('project-a');

console.log(currency);
console.log(y);
$ node project-b/bar.js
[Function: currency]
1

今回は両方のプロジェクトがcurrency.jsに依存していたが、ひとつのプロジェクトでしか依存していないパッケージも、ルートディレクトリのyarn.lockで管理し、ルートディレクトリのnode_modulesにインストールされる。

異なるバージョンのパッケージに依存している場合の挙動

./project-a/package.jsonを以下のように書き換えてみる。

{
  "name": "project-a",
  "version": "1.0.0",
  "main": "./foo.js",
  "dependencies": {
    "currency.js": "1.2.1"
  }
}

こうすると、project-aproject-bでそれぞれ、バージョンの異なるcurrency.jsに依存している状態になる。

この状態で、ルートディレクトリで$ yarnを実行する。

そうすると、./project-b/node_modules/currency.js/が作られ、そこにcurrency.js@2.0.0がインストールされる。

$ cat project-b/node_modules/currency.js/package.json | grep 'version'
  "version": "2.0.0",

./node_modules/には、currency.js@1.2.1がインストールされる。

$ cat ./node_modules/currency.js/package.json | grep 'version'
  "version": "1.2.1",

そしてルートディレクトリにあるyarn.lockに、両方のバージョンが記述される。

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


currency.js@1.2.1:
  version "1.2.1"
  resolved "https://registry.yarnpkg.com/currency.js/-/currency.js-1.2.1.tgz#18d42ac74d833795051f4a310c83f041013a882f"
  integrity sha512-7t0jYDZeQYCcaxpgwNOiBz8GaG/qdqTdo+kcTYgCuCNx1p2jLMpJt8h34TJ5INtN9jhryAiLK/atmjwUf13t4A==

currency.js@2.0.0:
  version "2.0.0"
  resolved "https://registry.yarnpkg.com/currency.js/-/currency.js-2.0.0.tgz#f23b5d8b963519e419ee051103fa44168eb5a8f1"
  integrity sha512-W1OqppUNYbxcTVKXI4oV9dkBlg/GWhYpEbvKzDl79JeljAEI6KNdjreX66/s59C7Fg+MjDwhV6RbT/gLVZJg9A==

project-aのプログラムがcurrency.jsを読み込む際、まず./project-a/node_modules/currency.jsを探す。
だが見つからない(そもそも./project-a/node_modulesがない)ので、親の階層のnode_modulesを探索する。
すると./node_modules/currency.jsが見つかるので、1.2.1currency.jsを読み込む。
同じロジックで、project-bのプログラムがcurrency.jsを読み込むと、2.0.0が読み込まれる。

yarn workspace コマンド

ここまで、動作確認のためにpackage.jsonを編集して依存パッケージを加えてきたが、一般的には$ yarn addコマンドでパッケージをインストールすることが多い。
ワークスペースを利用していても、それぞれのプロジェクトに移動すれば、通常通りインストールできる。

$ cd project-a
$ yarn add reselect

この場合も、ルートディレクトリのyarn.lockに記録され、ルートディレクトリのnode_modulesにインストールされる。

ルートディレクトリにいるときにインストールするには、$ yarn workspace プロジェクト名 add パッケージ名とすればよい。

ルートにいるときに以下のコマンドを実行すると、project-bdependenciesredux-thunkを追加する。

$ yarn workspace project-b add redux-thunk
$ cat project-b/package.json
{
  "name": "project-b",
  "version": "1.0.0",
  "main": "./bar.js",
  "dependencies": {
    "currency.js": "2.0.0",
    "project-a": "1.0.0",
    "redux-thunk": "^2.3.0"
  }
}

実行できるサブコマンドはaddだけでなく、$ yarn workspace プロジェクト名 サブコマンドで、任意のサブコマンドを実行できる。
例えば、xxxというプロジェクトからfooというパッケージへの依存を削除するには、以下のようにする。

$ yarn workspace xxx remove foo

各プロジェクトのpackage.jsonではなく、ルートディレクトリにあるpackage.jsonに依存関係を追加する時は、--ignore-workspace-root-checkフラグ(省略形は-W)を使う。

$ yarn add -D -W eslint
$ cat ./package.json
{
  "private": true,
  "workspaces": [
    "project-a",
    "project-b"
  ],
  "devDependencies": {
    "eslint": "^7.5.0"
  }
}

yarn workspaces run コマンド

$ yarn workspaces run スクリプト名で、各プロジェクトで定義してあるスクリプトを一度に実行できる。

試しに、各package.jsonscriptsフィールドで、fizzというスクリプトを定義する。

{
  "private": true,
  "workspaces": [
    "project-a",
    "project-b"
  ],
  "scripts": {
    "fizz": "echo fizz!! by root"
  },
  "devDependencies": {
    "eslint": "^7.5.0"
  }
}
{
  "name": "project-a",
  "version": "1.0.0",
  "main": "./foo.js",
  "scripts": {
    "fizz": "echo fizz!! by a"
  },
  "dependencies": {
    "currency.js": "1.2.1",
    "reselect": "^4.0.0"
  }
}
{
  "name": "project-b",
  "version": "1.0.0",
  "main": "./bar.js",
  "scripts": {
    "fizz": "echo fizz!! by b"
  },
  "dependencies": {
    "currency.js": "2.0.0",
    "project-a": "1.0.0",
    "redux-thunk": "^2.3.0"
  }
}

この状態で$ yarn workspaces run fizzを実行すると、次のような結果になる。

$ yarn workspaces run fizz
yarn workspaces v1.22.4

> project-a
yarn run v1.22.4
warning package.json: No license field
$ echo fizz!! by a
fizz!! by a
✨  Done in 0.03s.

> project-b
yarn run v1.22.4
warning package.json: No license field
$ echo fizz!! by b
fizz!! by b
✨  Done in 0.03s.
✨  Done in 0.43s.

./project-a/package.json./project-b/package.jsonに定義してあるfizzスクリプトが実行される。
./package.jsonfizzスクリプトは実行されない。これを実行するには、ルートディレクトリで$ yarn run fizzを実行すればよい。

$ yarn run fizz
yarn run v1.22.4
$ echo fizz!! by root
fizz!! by root
✨  Done in 0.03s.

参考資料