For experiment, I downloaded the source code of @typescript-eslint/eslint-plugin. This package has two peer dependencies:
{
"peerDependencies": {
"@typescript-eslint/parser": "^4.0.0",
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": "4.11.1",
"@typescript-eslint/scope-manager": "4.11.1",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
}
If I run npm list
after all dependencies will be installed, I'll get:
npm ERR! peer dep missing: eslint@^5.0.0 || ^6.0.0 || ^7.0.0, required by @typescript-eslint/[email protected]
npm ERR! peer dep missing: eslint@*, required by @typescript-eslint/[email protected]
Does it mean that npm
wants:
{
"peerDependencies": {
"@typescript-eslint/parser": "^4.0.0",
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"dependencies": {
"@typescript-eslint/parser": "^4.0.0",
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
// ...
}
}
Peer Dependencies: In package. json file, there is an object called as peerDependencies and it consists of all the packages that are exactly required in the project or to the person who is downloading and the version numbers should also be the same. That is the reason they were named as peerDependencies.
When to use peerDependencies? Peer dependencies really come into play when you're developing code that will be used by others, such as plugins and packages. If you're just working on a final product (i.e one that can't really be used inside another project), then you don't really have to worry about it.
"dependencies" : Packages required by your application in production. "devDependencies" : Packages that are only needed for local development and testing.
Dev Dependencies devDependencies are typically the tooling you use to build your project, but they're not actually included in the app's production bundle. For example, webpack and rollup would both be devDependencies . These are bundlers used to compile your app, but they're not part of your app.
@Daniel_Knights answered the question mostly to the point. But I'd like to add my two cents too. So here goes:
In order to understand this, it is important to understand the different types of dependencies in an NPM package. In general, there are 4 types of dependencies in NPM:
direct dependency (or simply dependency): These are dependencies which are absolutely necessary for an NPM package to function. If you are building a web application with express.js, then you absolutely want the express
to be installed for your application to boot up. So this would be a direct dependency for your application. These should be listed under the "dependencies": {}
section of package.json
.
development dependency: These are dependencies which are helpful while developing your application but not necessarily used by the application package to run. An example of such a dependency would be typescript
. NodeJS does not understand Typescript. So even though you could be writing your application in Typescript, after you run it through the typescript compiler, you are left with Javascript. So even though you need to add the typescript
package during development, you don't need it for your application to run AFTER it's compiled.
So if you add typescript
to your "devDependencies": {}
section in package.json
and do an npm install
, NPM will install both dependencies and devDependencies. At this stage, you can invoke your Typescript compiler to build your application. But after that, you can run npm prune --production
, and NPM will strip away all devDependencies from node_modules/
. This reduces your final application bundle size and keeps it free of any dev dependencies.
You should not refer to any dev dependency inside your source code without allowing for your code to safely and gracefully fallback to alternatives since the package will be removed on pruning.
"optionalDependencies": {}
section of package.json
. When you are specifying a dependency as optional, you letting NPM know that "Your program will use this dependency if it is available. If it's not, that's cool too. It will use something else."
A common scenario where this can help is in using database drivers. Database drivers written in JS are not particularly efficient or performant. So it's common to use a driver with native bindings (a JS library using a native (C/C++) package to run its tasks). But the problem is that for native bindings, the native package must be installed in the machine where the app is being run. This may not always be available. So we can specify a native library as an optional one. You can refer to this in JS code like:
var pg = require('pg-native'); // Native binding library
if (!pg) { // If it's not available...
pg = require('pg'); // ...use non native library.
}
So while installing packages using npm install
, NPM will attempt to install an optional dependency too. But if it isn't able to install (probably because the native binding isn't available), it'll not error out. It'll just post a warning and move on.
And now to the type of dependency in question...
"peerDependencies": {}
section of package.json
. Unlike the other three dependencies above, NPM will not attempt to install peer dependencies when doing npm install
. This is because NPM expects these dependencies to be provided by other dependencies.We will see why this makes sense, but we must take a very short detour to learn about how NPM structures dependencies within node_modules/
folder.
Let's do this with an example:
We'll init an npm package and install express
as a dependency:
$ npm install express --save
If we look at the node_modules/
directory now, we can see that it has the qs
package installed along with express
:
$ ls -l node_modules/
total 196
// ...more stuff...
drwxr-xr-x 3 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 express <---------- here is our express
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 finalhandler
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 forwarded
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 fresh
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 http-errors
drwxr-xr-x 4 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 iconv-lite
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 inherits
drwxr-xr-x 3 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 ipaddr.js
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 media-typer
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 merge-descriptors
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 methods
drwxr-xr-x 3 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 mime
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 mime-db
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 mime-types
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 ms
drwxr-xr-x 3 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 negotiator
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 on-finished
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 parseurl
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 path-to-regexp
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 proxy-addr
drwxr-xr-x 5 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 qs <---------- focus here for a bit
drwxr-xr-x 2 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 range-parser
// ...even more stuff ...
Now, there is no node_modules/
folder within express/
folder even though it has a package.json
:
$ ls -l node_modules/express/
total 132
-rw-r--r-- 1 rajshrimohanks rajshrimohanks 109589 Oct 26 1985 History.md
-rw-r--r-- 1 rajshrimohanks rajshrimohanks 1249 Oct 26 1985 LICENSE
-rw-r--r-- 1 rajshrimohanks rajshrimohanks 4607 Oct 26 1985 Readme.md
-rw-r--r-- 1 rajshrimohanks rajshrimohanks 224 Oct 26 1985 index.js
drwxr-xr-x 4 rajshrimohanks rajshrimohanks 4096 Dec 31 16:00 lib
-rw-r--r-- 1 rajshrimohanks rajshrimohanks 3979 Dec 31 16:00 package.json
And if you look at the package.json
of the express
package, you'll see that it requires qs
package with version 6.7.0
:
$ cat node_modules/express/package.json
{
// other stuff ...
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0", <-------------- this is what we are looking at
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
// ... more stuff ...
}
So express
requires qs
at version 6.7.0
so NPM put it alongside express
for it to use.
$ cat node_modules/qs/package.json
{
// ... stuff ...
"name": "qs",
"repository": {
"type": "git",
"url": "git+https://github.com/ljharb/qs.git"
},
"scripts": {
"coverage": "covert test",
"dist": "mkdirp dist && browserify --standalone Qs lib/index.js > dist/qs.js",
"lint": "eslint lib/*.js test/*.js",
"postlint": "editorconfig-tools check * lib/* test/*",
"prepublish": "safe-publish-latest && npm run dist",
"pretest": "npm run --silent readme && npm run --silent lint",
"readme": "evalmd README.md",
"test": "npm run --silent coverage",
"tests-only": "node test"
},
"version": "6.7.0" <---- this version
}
Now let's see what happens if we want to use qs
in our application BUT at version 6.8.0
.
$ npm install [email protected] --save
npm WARN [email protected] No description
npm WARN [email protected] No repository field.
+ [email protected]
added 2 packages from 1 contributor, updated 1 package and audited 52 packages in 0.796s
found 0 vulnerabilities
$ cat node_modules/qs/package.json
{
//... other stuff ...
"name": "qs",
"repository": {
"type": "git",
"url": "git+https://github.com/ljharb/qs.git"
},
"scripts": {
"coverage": "covert test",
"dist": "mkdirp dist && browserify --standalone Qs lib/index.js > dist/qs.js",
"lint": "eslint lib/*.js test/*.js",
"postlint": "eclint check * lib/* test/*",
"prepublish": "safe-publish-latest && npm run dist",
"pretest": "npm run --silent readme && npm run --silent lint",
"readme": "evalmd README.md",
"test": "npm run --silent coverage",
"tests-only": "node test"
},
"version": "6.8.0" <-------- the version changed!
}
NPM replaced the version with 6.8.0
which we want. But what about the needs of express
package which wants qs
at 6.7.0
? Don't worry, NPM takes care of it by giving express
its own local copy of qs
at 6.7.0
.
$ cat node_modules/express/node_modules/qs/package.json
{
// ... other stuff ...
"name": "qs",
"repository": {
"type": "git",
"url": "git+https://github.com/ljharb/qs.git"
},
"scripts": {
"coverage": "covert test",
"dist": "mkdirp dist && browserify --standalone Qs lib/index.js > dist/qs.js",
"lint": "eslint lib/*.js test/*.js",
"postlint": "editorconfig-tools check * lib/* test/*",
"prepublish": "safe-publish-latest && npm run dist",
"pretest": "npm run --silent readme && npm run --silent lint",
"readme": "evalmd README.md",
"test": "npm run --silent coverage",
"tests-only": "node test"
},
"version": "6.7.0" <----- just what express wants!
}
So you can see that NPM added a local node_modules
for express
alone and gave its own version. This is how NPM makes sure that both our application as well as express
are satisfied with their own requirements. But there is one key takeaway here:
"If more than one package require another package in common but at different versions, NPM will install multiple copies, for each one of them in order to satisfy them."
This may not be always ideal in some cases. Let's say our package wants to use qs
but we don't care what version it is as long as it is above version 6.0.0
and we are sure that some other package, like express
will also be used alongside (which has its own qs
at 6.7.0
). In that case, we may not want NPM to install another copy increasing the bulk. Instead we can specify qs
as a...peer dependency!
Now NPM won't install the peer dependency automatically. But will expect it to be provided by some other package.
In the case of @typescript-eslint/eslint-plugin
:
{
"peerDependencies": {
"@typescript-eslint/parser": "^4.0.0",
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": "4.11.1",
"@typescript-eslint/scope-manager": "4.11.1",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
}
@typescript-eslint/eslint-plugin
was intended to be use with @typescript-eslint/parser
and eslint
packages. There is no way you would be using @typescript-eslint/eslint-plugin
without using those because all these are part of a larger package eslint
which helps you lint Typescript and JS code. So you'd have installed eslint
anyways and that would be the only reason to use @typescript-eslint/eslint-plugin
.
Hence, the authors saw it fit to add themn as @typescript-eslint/eslint-plugin
doesn't care as long as you have any minor version of eslint
in the 5.x.x
, 6.x.x
or 7.x.x
series. Similarly for @typescript-eslint/eslint-parser
with version 4.x.x
.
Whew! That was quite a ride but hopefully this answered your question! :)
Now assume that I forked the @typescript-eslint/eslint-plugin and want all ERR! messages mentioned in question disappear. If I add eslint and parser to dependencies of forked package, peerDependencies becomes meaningless. Should I add them to devDependencies instead?
You could, but that would make the devDependencies
meaningless. What you must understand is that the package.json
file is just a manifest to instruct NPM what to do when someone else "installs" the package - either in another package as a dependency, or individually as a global package.
Regardless, the package.json
is like an instruction manual for NPM. It does not affect you, as a developer, in any way. So if all you want to do is add eslint
and @typescript-eslint/eslint-parser
for development purposes, you could simply do:
$ npm install --no-save eslint @typescript-eslint/eslint-parser
The --no-save
flag tells NPM not to add these to the package.json
but fetch the package and put it in node_modules/
directory regardless. When you are running your app, all it will do is, look into node_modules/
for the package presence and NOT package.json
. The purpose of package.json
is done after the install step.
Let me know if this clarifies your questions. I'll add more if required.
Happy New Year! :)
The peerDependencies
field is intended for use with libraries/plugins as a way of letting the installing app known which dependencies are required for it to work without adding extra bulk to the package itself in the dependencies
field.
From the docs:
As a package manager, a large part of npm's job when installing your dependencies is managing their versions. But its usual model, with a "dependencies" hash in package.json, clearly falls down for plugins. Most plugins never actually depend on their host package, i.e. grunt plugins never do
require("grunt")
, so even if plugins did put down their host package as a dependency, the downloaded copy would never be used. So we'd be back to square one, with your application possibly plugging in the plugin to a host package that it's incompatible with.
The idea is that you install the package in devDependencies
for developing the package and publish it without that dependency, then any app which tries to use your package and doesn't have this peer-dependency installed will receive an error:
npm ERR! peerinvalid The package flatiron does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer [email protected] wants flatiron@~0.1.9
npm ERR! peerinvalid Peer [email protected] wants flatiron@~0.3.0
The error you're receiving is simply saying that @typescript-eslint/eslint-plugin
requires you to install eslint
for it to work as it should.
So, the obvious answer is to run npm i -D eslint
to save it as a dev-dependency. But, this plugin is a sub-directory of the typescript-eslint
package, and the chance that the contributors forgot to add eslint
as a dev-dependency seems unlikely, so, it's safe to assume, it doesn't need to be installed for development.
Without knowing how it works internally, I'd say that, as typescript-eslint
is required for using @typescript-eslint/eslint-plugin
, developing any plugin for the parent package requires doing so through the parent package itself.
If you look in the contribution-guide, it mentions developing from the root-directory:
Developing in this repo is easy:
- First fork the repo, and then clone it locally.
- Create a new branch.
- In the root of the project, run yarn install.
- This will install the dependencies, link the packages and do a build.
- Make the required changes.
I'm no expert, so take this with a grain of salt, but, rather than working directly in the sub-directory, I think you need to be working on the plugin from the root of the project instead.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With