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.json
のmain
フィールドに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-b
はproject-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
そしてもちろん、問題なく読み込める。
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-a
とproject-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.1
のcurrency.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-b
のdependencies
にredux-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.json
のscripts
フィールドで、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.json
のfizz
スクリプトは実行されない。これを実行するには、ルートディレクトリで$ yarn run fizz
を実行すればよい。
$ yarn run fizz
yarn run v1.22.4
$ echo fizz!! by root
fizz!! by root
✨ Done in 0.03s.
参考資料