npm workspaceとnpm installの挙動確認

環境

% node -v
v16.13.0
% npm -v
8.11.0

概要

npm workspaceでnpm installを実行すると
 1. ワークスペースルート
 2. (バージョンが異なるものは)各々のワークスペース
の順にインストールされる

構成

npm-workspace % tree . -a -I node_modules 
.
├── .gitignore
├── .npmrc
├── app-a
│   └── package.json
├── app-b
│   └── package.json
├── package-lock.json
└── package.json
save-exact=true
{
  "name": "npm-workspace",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "workspaces": [
    "app-a",
    "app-b"
  ]
}

挙動の確認

 %npm i rimraf -w app-a
 %npm i rimraf@2 -w app-b

結果A

 % tree .     
.
├── app-a
│   └── package.json
├── app-b
│   ├── node_modules
│   │   └── rimraf
│   │       ├── LICENSE
│   │       ├── README.md
│   │       ├── bin.js
│   │       ├── package.json
│   │       └── rimraf.js
│   └── package.json
├── node_modules
│   ├── app-a -> ../app-a
│   ├── app-b -> ../app-b
│   ├── balanced-match
│   │   ├── LICENSE.md
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── brace-expansion
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── concat-map
│   │   ├── LICENSE
│   │   ├── README.markdown
│   │   ├── example
│   │   │   └── map.js
│   │   ├── index.js
│   │   ├── package.json
│   │   └── test
│   │       └── map.js
│   ├── fs.realpath
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── old.js
│   │   └── package.json
│   ├── glob
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── common.js
│   │   ├── glob.js
│   │   ├── package.json
│   │   └── sync.js
│   ├── inflight
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inflight.js
│   │   └── package.json
│   ├── inherits
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inherits.js
│   │   ├── inherits_browser.js
│   │   └── package.json
│   ├── minimatch
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── minimatch.js
│   │   └── package.json
│   ├── once
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── once.js
│   │   └── package.json
│   ├── path-is-absolute
│   │   ├── index.js
│   │   ├── license
│   │   ├── package.json
│   │   └── readme.md
│   ├── rimraf
│   │   ├── CHANGELOG.md
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── bin.js
│   │   ├── package.json
│   │   └── rimraf.js
│   └── wrappy
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       └── wrappy.js
├── package-lock.json
└── package.json

21 directories, 65 files

package-lock.jsonを削除し、コマンドの実行順を変えてみる

% rm package-lock.json 
% rm -rf node_modules && rm -rf app-b/node_modules
%npm i rimraf@2 -w app-b
 %npm i rimraf -w app-a

結果

% tree .
.
├── app-a
│   ├── node_modules
│   │   └── rimraf
│   │       ├── CHANGELOG.md
│   │       ├── LICENSE
│   │       ├── README.md
│   │       ├── bin.js
│   │       ├── package.json
│   │       └── rimraf.js
│   └── package.json
├── app-b
│   └── package.json
├── node_modules
│   ├── app-a -> ../app-a
│   ├── app-b -> ../app-b
│   ├── balanced-match
│   │   ├── LICENSE.md
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── brace-expansion
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   └── package.json
│   ├── concat-map
│   │   ├── LICENSE
│   │   ├── README.markdown
│   │   ├── example
│   │   │   └── map.js
│   │   ├── index.js
│   │   ├── package.json
│   │   └── test
│   │       └── map.js
│   ├── fs.realpath
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── index.js
│   │   ├── old.js
│   │   └── package.json
│   ├── glob
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── common.js
│   │   ├── glob.js
│   │   ├── package.json
│   │   └── sync.js
│   ├── inflight
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inflight.js
│   │   └── package.json
│   ├── inherits
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── inherits.js
│   │   ├── inherits_browser.js
│   │   └── package.json
│   ├── minimatch
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── minimatch.js
│   │   └── package.json
│   ├── once
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── once.js
│   │   └── package.json
│   ├── path-is-absolute
│   │   ├── index.js
│   │   ├── license
│   │   ├── package.json
│   │   └── readme.md
│   ├── rimraf
│   │   ├── LICENSE
│   │   ├── README.md
│   │   ├── bin.js
│   │   ├── package.json
│   │   └── rimraf.js
│   └── wrappy
│       ├── LICENSE
│       ├── README.md
│       ├── package.json
│       └── wrappy.js
├── package-lock.json
└── package.json

以上の結果より、コマンドが実行された順番でワークスペースルートにインストールされることを確認できた。

npm installの実行順について

下記のような状態で

app-a/package.json

...
  "dependencies": {
    "rimraf": "3.0.2"
  },
...

app-b/package.json

...
  "dependencies": {
    "rimraf": "2.7.1"
  },
...
  • node_modulesとpackage-lock.jsonを削除してnpm iしたところAと同じ構造になることを確認
  • package.jsonのworkspaceの順序を入れ替えてnpm iしてもAと同じ構造になることを確認

package.json

  "workspaces": [
    "app-b",
    "app-a"
  ]
  • app-bの名前を変えてnpm iを実行すると、ワークスペースルートにrimraf@2.7.1がインストールされることを確認
%mv app-b app
% npm i 

よってnpm iの実行順序はディレクトリ名順で実行されることがわかった。

まとめ

npm workspaceとnpm installの挙動について初めて見たときよくわからなかったのでまとめた。