Npm allows multiple types of dependencies, two of which are
dependencies
and peerDependencies
. Semantically, they refer to different things:
peerDependencies
expresses the compatibility of a package with a host tool or library, usually
referred to as a plugin, while dependencies
expresses dependencies in the general sense.
Despite being two distinct types of dependencies, since v7, npm installs both types of dependencies by default. In this post, we explore the answer to the question: What are the differences between the effects of the two types of dependencies?
For the impatient, feel free to jump to the conclusion.
Basic Situation
In a basic situation, a package is specified in dependencies
or peerDependencies
and is not
marked as optional, and there is no conflict of dependencies. The doc does not expressly state the
differences in the effects of this situation, thus we ran a couple of small experiments with npm
version 10.8.0
.
The experiments included two packages, each containing a package.json
file with a single
dependencies
or peerDependencies
entry, respectively. We created a package.json
file and put
it in the dependencies/
directory:
{
"name": "foo",
"version": "1.0.0",
"dependencies": {
"react": "^18.3.1"
}
}
Similarly, we replaced "dependencies"
with "peerDependencies"
and put it in the
peerDependencies/
directory:
{
"name": "foo",
"version": "1.0.0",
"peerDependencies": {
"react": "^18.3.1"
}
}
npm install
within the Package Directories
We ran npm install
in each of the two directories. Then we ran
diff -r dependencies/ peerDependencies/
. Other than package.json
, only package-lock.json
and
node_modules/.package-lock.json
were different:
--- dependencies/package-lock.json 2024-05-19 15:48:24.581806109 -0700
+++ peerDependencies/package-lock.json 2024-05-19 15:48:28.189793011 -0700
@@ -7,19 +7,21 @@
"": {
"name": "foo",
"version": "1.0.0",
- "dependencies": {
+ "peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "peer": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -31,6 +33,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
The same applied to node_modules/.package-lock.json
(omitted in the diff above).
This implies that:
- The generated
package-lock.json
files only differed in indicating whether the dependency is peer or not. - All dependency package files installed in each directory were the same.
Installing the Two Packages
In this subsection, we installed the two packages above using npm
. First, we packed the package by
running npm pack
in both the dependencies/
and peerDependencies/
directory, which generates a
tarball named foo-1.0.0.tgz
in each directory.
Then we created two directories named dependencies-user
and peerDependencies-user
, respectively.
In each of the two directories, we ran npm install ../dependencies/foo-1.0.0.tgz
and npm install ../peerDependencies/foo-1.0.0.tgz
, respectively. Then we ran diff -r dependencies-user/ peerDependencies-user/
:
diff --color -u --color -r dependencies-user/node_modules/foo/package.json peerDependencies-user/node_modules/foo/package.json
--- dependencies-user/node_modules/foo/package.json 2024-05-21 16:24:16.720145421 -0700
+++ peerDependencies-user/node_modules/foo/package.json 2024-05-21 16:24:10.848169061 -0700
@@ -1,7 +1,7 @@
{
"name": "foo",
"version": "1.0.0",
- "dependencies": {
+ "peerDependencies": {
"react": "^18.3.1"
}
}
diff --color -u --color -r dependencies-user/package.json peerDependencies-user/package.json
--- dependencies-user/package.json 2024-05-21 16:01:04.069562379 -0700
+++ peerDependencies-user/package.json 2024-05-21 16:24:10.856169029 -0700
@@ -1,5 +1,5 @@
{
"dependencies": {
- "foo": "file:../dependencies/foo-1.0.0.tgz"
+ "foo": "file:../peerDependencies/foo-1.0.0.tgz"
}
}
diff --color -u --color -r dependencies-user/package-lock.json peerDependencies-user/package-lock.json
--- dependencies-user/package-lock.json 2024-05-21 16:24:16.728145389 -0700
+++ peerDependencies-user/package-lock.json 2024-05-21 16:24:10.856169029 -0700
@@ -1,24 +1,26 @@
{
- "name": "dependencies-user",
+ "name": "peerDependencies-user",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
- "foo": "file:../dependencies/foo-1.0.0.tgz"
+ "foo": "file:../peerDependencies/foo-1.0.0.tgz"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -31,6 +33,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -40,9 +43,9 @@
},
"node_modules/foo": {
"version": "1.0.0",
- "resolved": "file:../dependencies/foo-1.0.0.tgz",
- "integrity": "sha512-asRRhQKMjcFJQLyC47nSqG9Rv6IsCsjnfvt4bxLs89KSWbOR+csGllJ1fIojRO4lFKweJFi79ZdvkFcigwhP+A==",
- "dependencies": {
+ "resolved": "file:../peerDependencies/foo-1.0.0.tgz",
+ "integrity": "sha512-Rddf3EYuMq8GTrvn+oKIVX9C3MZzP8kVnFbXqxbkm0e346SJ1K6xJ7bO8OIy96tFI+tMX8vqWJ+a/l6pF8aPpw==",
+ "peerDependencies": {
"react": "^18.3.1"
}
}
We observed that there was no difference other than the names of the dependencies and indications of
whether the dependency was peer or not. (Diff in node_modules
was the same as that in the previous
subsection and is thus omitted.)
Install the Two Packages as Peer Dependencies
After emptying the dependencies-user/
and peerDependencies-user/
directories, we repeated the
experiment in the previous subsection, but with npm install
replaced with npm install --save-peer
. The result was similar:
diff --color -u --color -r dependencies-user/node_modules/foo/package.json peerDependencies-user/node_modules/foo/package.json
--- dependencies-user/node_modules/foo/package.json 2024-05-21 16:30:57.282613821 -0700
+++ peerDependencies-user/node_modules/foo/package.json 2024-05-21 16:31:13.026556104 -0700
@@ -1,7 +1,7 @@
{
"name": "foo",
"version": "1.0.0",
- "dependencies": {
+ "peerDependencies": {
"react": "^18.3.1"
}
}
diff --color -u --color -r dependencies-user/package.json peerDependencies-user/package.json
--- dependencies-user/package.json 2024-05-21 16:30:57.294613777 -0700
+++ peerDependencies-user/package.json 2024-05-21 16:31:13.038556061 -0700
@@ -1,5 +1,5 @@
{
"peerDependencies": {
- "foo": "file:../dependencies/foo-1.0.0.tgz"
+ "foo": "file:../peerDependencies/foo-1.0.0.tgz"
}
}
diff --color -u --color -r dependencies-user/package-lock.json peerDependencies-user/package-lock.json
--- dependencies-user/package-lock.json 2024-05-21 16:30:57.294613777 -0700
+++ peerDependencies-user/package-lock.json 2024-05-21 16:31:13.042556045 -0700
@@ -1,11 +1,11 @@
{
- "name": "dependencies-user",
+ "name": "peerDependencies-user",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"peerDependencies": {
- "foo": "file:../dependencies/foo-1.0.0.tgz"
+ "foo": "file:../peerDependencies/foo-1.0.0.tgz"
}
},
"node_modules/js-tokens": {
@@ -43,10 +43,10 @@
},
"node_modules/foo": {
"version": "1.0.0",
- "resolved": "file:../dependencies/foo-1.0.0.tgz",
- "integrity": "sha512-asRRhQKMjcFJQLyC47nSqG9Rv6IsCsjnfvt4bxLs89KSWbOR+csGllJ1fIojRO4lFKweJFi79ZdvkFcigwhP+A==",
+ "resolved": "file:../peerDependencies/foo-1.0.0.tgz",
+ "integrity": "sha512-Rddf3EYuMq8GTrvn+oKIVX9C3MZzP8kVnFbXqxbkm0e346SJ1K6xJ7bO8OIy96tFI+tMX8vqWJ+a/l6pF8aPpw==",
"peer": true,
- "dependencies": {
+ "peerDependencies": {
"react": "^18.3.1"
}
}
(Diff in node_modules was the same as those in previous subsections and is thus omitted.)
Conflicting Dependencies
What if there are conflicts in dependencies?
Continuing the experimental setup in the previous section, we emptied the dependencies-user/
directory and added a package.json
with the following content:
{
"dependencies": {
"foo": "file:../dependencies/foo-1.0.0.tgz",
"react": "^17"
}
}
Here, "react": "^17"
is incompatible with the dependency of foo
, which requires "react": "^18.3.1"
.
Similarly, we emptied the peerDependencies-user/
directory and added a package.json
with the
following content:
{
"dependencies": {
"foo": "file:../peerDependencies/foo-1.0.0.tgz",
"react": "^17"
}
}
Then, run npm install
in dependencies-user/
and peerDependencies-user/
, respectively. while
npm install
in dependencies-user/
succeeded, npm install
in peerDependencies-user
failed
to resolve dependencies:
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: undefined@undefined
npm error Found: react@17.0.2
npm error node_modules/react
npm error react@"^17" from the root project
npm error
npm error Could not resolve dependency:
npm error peer react@"^18.3.1" from foo@1.0.0
npm error node_modules/foo
npm error foo@"file:../peerDependencies/foo-1.0.0.tgz" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
This seems to be what this quoted part of npm doc on peerDependencies
refers
to:
Trying to install another plugin with a conflicting requirement may cause an error if the tree cannot be resolved correctly.
Further inspection into dependencies-user/
showed that a copy of react
version 18 lived in
node_modules/foo/node_modules/react
, and a copy of react
version 17 lived in
node_modules/react
. In other words, two versions of react
existed in dependencies-user/
, which
might cause runtime errors if foo
was meant to be a plugin to an existing react
package (such as
a React component) instead of bringing its own dependency of react
.
Optional Dependencies
What if a dependency is specified as optional? The doc of npm has given a clear answer.
Optional Dependencies in optionalDependencies
An optional dependency can be specified via optionalDependencies
. The doc of
optionalDependencies
reads:
Running
npm install --omit=optional
will prevent these dependencies from being installed.
In other words, unless explicitly omitted, npm installs optional dependencies specified in
optionalDependencies
by default.
Optional Peer Dependencies
An optional peer dependency can be specified via peerDependenciesMeta
. The doc of
peerDependenciesMeta
reads:
Npm will not automatically install optional peer dependencies. This allows you to integrate and interact with a variety of host packages without requiring all of them to be installed.
Summary
By default, npm installs optional dependencies specified in optionalDependencies
, but not optional
peer dependencies.
Conclusion
- When there is no optional dependency or conflict of dependencies, our experiments showed that
there is no difference in the effects of specifying a package in
dependencies
orpeerDependencies
, other than indications of whether they are peer dependencies or not. (The differences in their semantics for human readers still hold.) - A conflict of dependencies can occur when package
bar
depends onfoo
, and packagebaz
depends onbar
and an incompatible version offoo
. When there is such kind of conflict of dependencies, our experiments showed that npm reports an error iffoo
is a peer dependency ofbar
, but installs two versions offoo
if the dependency is specified independencies
. - When there are optional dependencies, by default, npm installs packages included in
optionalDependencies
, but not those specified inpeerDependenciesMeta
as optional peer dependencies.