@Daniel_Knights answered the question mostly to the point. But I'd like to add my two cents too. So here goes:
Types of dependencies in NPM:
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 package 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.
- optional dependency: These are dependencies you can specify inside the
"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...
- peer dependency: As you already know, these are dependencies you specify inside the
"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.
How NPM stores dependencies
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.
So finally, coming to your case...
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! :)
Edit based on comment:
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! :)
Edit to address @Daniel Kaplan's comments:
Correct me if I'm wrong, but the official documentation presents a completely different scenario: the package using your package provides the peerDependency.
The scenario I'm presenting here is from a perspective of our app (not a lib!) being a NodeJS application. If we are creating a library instead, then we might specify our library as a peer dependency as well if our case requires it. Not much changes in terms of functionality, except that we are looking at the situation from the opposite spectrum - you'll be specifying the peer dependency in the app which uses our library.
When you are running your app, all it will do is, look into node_modules/ for the package presence. Does that mean when I run my package locally, my devDependencies are available to the production code?
Yes, they are. The point of the package.json file is to tell npm what to do when you use npm xxx commands. Your app, on the other hand, doesn't care about npm. It's totally just going to run on the NodeJS runtime and the runtime will import any package as long as they are in the node_modules/ folder.
This is why we should be careful not to import and use a devDependency in our application code, even though we totally can. Because, running npm prune --production will remove all devDependencies. You might ask at this point, why have them at all? Because, we can have scenarios where we might have a package useful only during development. For example, I use the rimraf package to delete the dist/ folder whenever I'm rebuilding my application code during development. I can invoke this directly from a script within package.json, but I'd need the package installed in order for it to work. So I can install it as a devDependency since I won't be needing that in production.
Hopefully, this gives you some clarity. :)