author-image

John Mc Quillan

npm Peer Dependencies

We’ll assume that you have a basic working knowledge of npm. However, quite often people struggle with the different types of dependencies and, in particular Peer Dependencies. If so, this post will improve your understanding. There are actually five different dependency types defined by npm:

  • normal dependencies
  • dev dependencies
  • peer dependencies
  • optional dependencies
  • bundled dependencies

Of these, normal and dev dependencies are generally well understood and the use cases for optional and bundled dependencies are few and far between, which brings us nicely to the subject of this post, peer dependencies.

We’ll describe what they are and discuss when it might be appropriate to use them.

Dependencies vs. Peer Dependencies

If you’ve used nodejs at all, you will have come across normal dependencies in package.json. They are used to describe the modules that your application depends on. They will always be included in your built module. They look a bit like this:

"dependencies": {
  "react": "^17.0.1",
  "react-dom": "^17.0.1"
}

And, you’ve probably also seen dev dependencies, which are used to describe dependencies that are used as part of your development process but not needed in you product build. They are installed in your dev environment by npm install but they are not included in your built module.Test and build tools are commonly in the dev dependencies, for example:

"devDependencies": {             
  "@testing-library/jest-dom": "^5.11.6",
  "@testing-library/react": "^11.2.2",    
  "@testing-library/user-event": "^12.6.0"
}

Peer dependencies effectively declare a dependency without including the dependency in your built module. When an application includes your module, that application will in turn need to include the declared dependency. With npm version 4 through to 6, a warning is issued when you run npm install to remind you to install the peer dependencies. Prior to version 4, npm automatically included peer dependencies if they weren’t explicitly included. That behaviour led to too much complexity in dependency tree calculation and it was dropped in version 4. With npm version 7, a brand new dependency tree manager is being introduced. With this, automatic inclusion of peer dependencies is returning. You can read about the Arborist dependency tree manager here.

When to Use Peer Dependencies

Sometimes, and particularly when you are building a library that will be used by other applications, you will have a dependency that will almost certainly also be a core dependency of those other applications. For example, if you are building a library of React components, React will be a dependency you need, but almost certainly the application that uses your library will need React. This is where we use Peer Dependencies.

Peer dependencies provide the details of what the host application is expected to provide. Taking our React example, our peer dependencies might look like this:

"peerDependencies": {
  "react": "^17.0.1",
  "react-dom": "^17.0.1"
}

Semantic Versioning

When specifying the allowed versions of a package in a peer dependency in our package.json, we often want to specify more liberal version ranges than we typically use for normal or dev dependencies. As a brief recap, semantic versioning or semver is used in package.json to specify the versions of a dependency that are compatible with the package described by the package.json. Semantic versioning generally defines the version of a package using three digits, major.minor.patch.

Most normal or dev dependencies use one of two specifiers:

  • tilde (~) to allow newer patch level versions of a package
  • caret (^) to allow newer minor level versions of a package

Caret is the default when we use npm install or npm install --save-dev. The npm option --save-prefix can be used to change the specifier used.

To refresh your knowledge of npm’s semver usage, we’d recommend reading the official docs.

When it comes to peer dependencies, we are generally specifying the versions of a dependency that our package can work with, rather than the version we’d prefer to use. This sometimes leads to broader specifications than those provided by the tilde and caret specifiers. For example, we might know that our package can work with more than one major version of a dependency. Consider that we might be building a React Library that works for both React 16 versions greater than minor version 8 an also works with React 17 and is optimistically expected to work with future minor updates to React 17. Our specifier might look like this:

"peerDependencies": {
  "react": "16.8 - 17"
}

A useful tool for checking your specifiers is the semver calculator, available on the npm web site.

Unmet Dependencies When Testing

When you use peer dependencies, npm will not automatically install those dependencies (see comments above in respect to npm version 7). This can lead to errors when you are running tests on your package, although you will get warnings when executing npm to prompt you to install the peer dependencies. One way to avoid these warnings and errors is to also include the peer dependencies as dev dependencies. That way, they will be available for local testing but will still be peer dependencies in the published npm module.

If you’d like to keep up with new posts, please consider following us.

SO WHAT DO YOU THINK ?

We love to talk about ideas, innovation, technology and just about anything else. Really, we love to talk.

Contact fathom
a