[BUG] Installing a package globally also installs its peerDependencies even if they are already available globally
ericmorand opened this issue · 8 comments
Is there an existing issue for this?
- I have searched the existing issues
This issue exists in the latest npm version
- I am using the latest npm
Current Behavior
When installing a package globally, its peer dependencies are also always installed in its own node_modules
directory even when these peer dependencies are already installed globally.
Expected Behavior
I expect that the peer dependencies available globally are not installed globally twice when they are the same version, just like npm
behaves when installing locally.
Having multiple versions of the same global dependency creates some issues with typedoc (probably among some other tools) where typedoc ends up installed twice:
- globally
- under each theme
node_modules
directory
Obviously, when executing the typedoc binary to build a documentation, the executed instance of typedoc is not the one that is required by the theme, leading to the infamous error:
$ typedoc src/lib.ts --out public --plugin typedoc-material-theme
[info] Loaded plugin typedoc-material-theme
[warning] TypeDoc has been loaded multiple times. This is commonly caused by plugins which have their own installation of TypeDoc. The loaded paths are:
/home/ericmorand/.nvm/versions/node/v18.19.0/lib/node_modules/typedoc
/home/ericmorand/.nvm/versions/node/v18.19.0/lib/node_modules/typedoc-material-theme/node_modules/typedoc
To confirm that there is no need to install "locally" peer dependencies that are already installed globally, I deleted the /home/ericmorand/.nvm/versions/node/v18.19.0/lib/node_modules/typedoc-material-theme/node_modules/typedoc
directory, rerun the command and everything went fine.
Which is to be expected: the theme eventually requires typedoc
which is then resolved to the package installed one level above the theme, as per the resolution rule of node:
https://nodejs.org/api/modules.html#loading-from-node_modules-folders
If the module identifier passed to require() is not a core module, and does not begin with '/', '../', or './', then Node.js starts at the directory of the current module, and adds /node_modules, and attempts to load the module from that location. Node.js will not append node_modules to a path already ending in node_modules.
If it is not found there, then it moves to the parent directory, and so on, until the root of the file system is reached.
Workarounds:
- Removing the theme
node_modules/typedoc
directory which may be challenging in a build system where we don't necessarily control where global packages are located - Installing typedoc and the themes locally which makes
npm
respect the rule of not installing twice the same dependency - but it means all our projects need to be reworked because none of them include typedoc or its themes: our build system is responsible for the documentation building and it is where we enforce some common typedoc themes and settings for all our products, for consistency and convenience - Installing the theme using the
--legacy-peer-deps
flag which is the best solution we found - it is simple and it works :party
Steps To Reproduce
- Install npm 10
- Install typedoc globally
npm i -g typedoc
- Install a typedoc theme globally
npm i -g typedoc-material-theme
- Look into the global
node_modules
directory: you'll findtypedoc
installed under both the globalnode_modules
directory and undernode_modules/typedoc-material-theme/node_modules
- Also note how they are exactly the same version
Environment
- npm: 10.2.4
- Node.js: 18.19.0
- OS Name: Ubuntu
- npm config:
; "user" config from /home/ericmorand/.npmrc
//registry.npmjs.org/:_authToken = (protected)
; node bin location = /home/ericmorand/.nvm/versions/node/v18.19.0/bin/node
; node version = v18.19.0
; npm local prefix = /home/ericmorand/Projects/twing
; npm version = 10.2.3
; cwd = /home/ericmorand/Projects/twing
; HOME = /home/ericmorand
; Run `npm config ls -l` to show all defaults.
This is expected since global modules can’t be required.
@ljharb they can and they are. The proof in the description of the issue: typedoc themes require typedoc successfully when both are installed globally.
You are confusing non-global modules - that can't require global modules - and global modules that can.
I want to make clear that node.js has no specification at all that prevents global modules from being required. The specification of the require
algorithm organically makes global modules likely to not be requirable, but it is because they are likely to not be located in a directory that is among the parents of the module that calls require
. That's why the NODE_PATH
variable exists: to add some arbitrary locations to be considered by the require algorithm.
So, it is totally wrong to say "global modules can’t be required".
That said, to keep it simple, the rest of this comment assumes that NODE_PATH
was not configured to include the global modules folder and that by "non global" module we mean "a module that is not installed in a directory that is a child of the global modules one".
Node.js walks the filesystem up to the root when looking for a module to load. So, yes, you are right, global modules are not considered when a module is required from a non global package.
/home/me/my-local-package/node_modules
/home/me/node_modules
/home/node_modules
/node_modules
In this case global modules are not considered by the resolution algorithm.
But global modules are installed in the global modules directory. So walking up the filesystem eventually ends up in the root of the global module directory, allowing Node.js to actually find the required module.
/home/me/global-modules/a-global-package/node_modules
/home/me/global-modules/node_modules
/home/me/node_modules
/home/node_modules
/node_modules
Here the global modules are considered by Node.js. So:
Global modules can require global modules
If true that’s still surprising to me, but fair enough.
This is intentional. There is no overarching package.json
that manages the global package space. It is not a package in and of itself. Notice how there is NOTHING in this directory except a node_modules folder
$ ls (npm prefix -g)/lib
node_modules/
Therefore npm can't treat it like a package, specifically it can't know about other packages that are installed alongside each other. Each package installed globally is done as a discrete unit. Allowing the fact that global package could require each other to be relied on would mean that npm could remove a peer of an existing package if asked.
TLDR the global package namespace is not a single package namespace. Each globally installed package is treated as its own discrete installation and packages that happened to rely on cross-requiring across the global namespace may experience unexpected results.
There is also the fact that when one is installing into the global namespace there is no way to know what other packages may be requiring into it. The current npm contract is that npm install
will always leave your current project's tree in a fully reified state. Consider for instance a fresh clone of a project repo with nothing installed. If I typed something like npm install semver
where semver
was a package that wasn't already installed, when npm was done every dependency would also be installed in node_modules
.
There is no way to guarantee that a global install could keep this project with non-global packages that require into the global namespace
@wraithgar thanks a lot. Just to be sure that I understood perfectly: peer dependencies are automatically installed only when a package is installed globally, and must be installed manually when a package is installed locally, right? Ot is it automatic in both cases?
If it is automatic in both cases, what is the point of having npm install
emit a warning if a peer dependency is not already installed? If npm install
installs it anyway, surely the warning is useless, what do you think?
https://docs.npmjs.com/cli/v7/configuring-npm/package-json#peerdependenciesmeta
When a user installs your package, npm will emit warnings if packages specified in peerDependencies are not already installed.
About the issue with typedoc I discussed with the team and we will have our pipeline install typedoc and its themes locally (instead of globally) after npm install
. This is a trivial change and it will solve the issue for good. 🥳
It's not always fully solvable by a computer (think P=NP hard); it's always best to have peer deps explicitly installed.
Certainly in general, installing things locally is always better than globally, so that's the best outcome.
The docs for peerDependencies
clarify further:
In npm versions 3 through 6, peerDependencies were not automatically installed, and would raise a warning if an invalid version of the peer dependency was found in the tree. As of npm v7, peerDependencies are installed by default.
Trying to install another plugin with a conflicting requirement may cause an error if the tree cannot be resolved correctly. For this reason, make sure your plugin requirement is as broad as possible, and not to lock it down to specific patch versions.
It looks like the language in peerDependenciesMeta
is describing the npm 6 behavior. A PR updating that section would be helpful.