Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I duplicate "peerDependencies" in "dependencies" field of "package.json"?

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"
     // ...
  }
}
like image 423
Takeshi Tokugawa YD Avatar asked Dec 29 '20 04:12

Takeshi Tokugawa YD


People also ask

What is peerDependencies in package json?

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.

Should I use 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.

What is the difference between devDependencies and dependencies in package json?

"dependencies" : Packages required by your application in production. "devDependencies" : Packages that are only needed for local development and testing.

Are Dev dependencies bundled?

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.


2 Answers

@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:

  1. 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.

  2. 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.

  1. 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...

  1. 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! :)

like image 128
Rajshri Mohan K S Avatar answered Oct 19 '22 05:10

Rajshri Mohan K S


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.

like image 31
Daniel_Knights Avatar answered Oct 19 '22 04:10

Daniel_Knights