It turns out, when two packages rely on the same module with a compatible version number in the dependencies, npm (with workspaces) will install the package only in root/node_modules. Otherwise, it will install in the node_modules of package(s).
In this example the same react-dom version is shared between packages:
customer-ui@1.0.0 /home/ubuntu/name/customer-ui
├─┬ components@0.15.0 -> ./packages/components
│ ├─┬ react-cosmos@5.7.2
│ │ └─┬ react-cosmos-shared2@5.7.1
│ │ └─┬ react-element-to-jsx-string@14.3.4
│ │ └── react-dom@17.0.2 deduped
│ ├── react-dom@17.0.2
│ ├─┬ react-modal@3.15.1
│ │ └── react-dom@17.0.2 deduped
│ ├─┬ react-to-print@2.14.7
│ │ └── react-dom@17.0.2 deduped
│ └─┬ styled-components@5.3.5
│ └── react-dom@17.0.2 deduped
└─┬ embedded@0.1.0 -> ./packages/embedded
└── react-dom@17.0.2 deduped
And npm installed it only in the root/node_modules dir:
❯ ls node_modules/react-dom
LICENSE build-info.json index.js profiling.js server.js test-utils.js
README.md cjs package.json server.browser.js server.node.js umd
❯ ls packages/embedded/node_modules/react-dom
ls: cannot access 'packages/embedded/node_modules/react-dom': No such file or directory
❯ ls packages/components/node_modules/react-dom
ls: cannot access 'packages/components/node_modules/react-dom': No such file or directory
However, for a package with incompatible version between packages, it's different:
❯ npm ls argparse
customer-ui@1.0.0 /home/ubuntu/name/customer-ui
├─┬ @components@0.15.0 -> ./packages/components
│ └─┬ eslint@8.17.0
│ └─┬ js-yaml@4.1.0
│ └── argparse@2.0.1
└─┬ embedded@0.1.0 -> ./packages/embedded
├─┬ eslint@6.8.0
│ └─┬ js-yaml@3.14.1
│ └── argparse@1.0.10
├─┬ jest-preset-preact@4.0.5
│ └─┬ babel-jest@27.5.1
│ └─┬ babel-plugin-istanbul@6.1.1
│ └─┬ @istanbuljs/load-nyc-config@1.1.0
│ └─┬ js-yaml@3.14.1
│ └── argparse@1.0.10
└─┬ microbundle@0.12.4
├─┬ cssnano@4.1.11
│ ├─┬ cosmiconfig@5.2.1
│ │ └─┬ js-yaml@3.14.1
│ │ └── argparse@1.0.10
│ └─┬ cssnano-preset-default@4.0.8
│ └─┬ postcss-svgo@4.0.3
│ └─┬ svgo@1.3.2
│ └─┬ js-yaml@3.14.1
│ └── argparse@1.0.10
└─┬ rollup-plugin-postcss@2.9.0
└─┬ postcss-load-config@2.1.2
└─┬ cosmiconfig@5.2.1
└─┬ js-yaml@3.14.1
└── argparse@1.0.10
embedded package demands argparse@1.0.10 and embedded demands argparse@2.0.1.
For these, npm installs them in multiple places:
❯ ls node_modules/argparse
CHANGELOG.md LICENSE README.md argparse.js lib package.json
❯ ls packages/components/argparse
ls: cannot access 'packages/components/argparse': No such file or directory
❯ ls packages/embedded/node_modules/argparse
CHANGELOG.md LICENSE README.md index.js lib package.json
In case it helps, this is a snippet of root/package.json:
{
"name": "customer-ui",
"version": "1.0.0",
"workspaces": [
"packages/*"
]
}
Thanks to @Stf_F for answering this in the comment above.