Local Project Basic Steps | Part 2 *

Update Node Dependencies to latest versionSemantic versioning rulesPeer DependenciesNPM Errors / Breakdowns

How to update all the Node dependencies to their latest version

How do you update all the npm dependencies store in the package.json file, to their latest version available?

When you install a package using npm install , the latest available version of the package is downloaded and put in the node_modules folder, and a corresponding entry is added to the package.json and package-lock.json files that are present in your current folder. npm calculates the dependencies and installs the latest available version of those as well.

Let's say you install cowsay , a cool command line tool that lets you make a cow say things.

When you npm install cowsay , this entry is added to the package.json file:


{
"dependencies": {
"cowsay": "^1.3.1"
}
}

and this is an extract of package-lock.json, where I removed the nested dependencies for clarity:


{

"requires": true,

"lockfileVersion": 1,

"dependencies": {

"cowsay": {

"version": "1.3.1",

"resolved": "",

"integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHz

VAYeIYFF5po5NjRrgefnRMQ==",

"requires": {

"get-stdin": "^5.0.1",

"optimist": "~0.6.1",

"string-width": "~2.1.1",

"strip-eof": "^1.0.0"

}

}

}

}

 

Now those 2 files tell us that we installed version 1.3.1 of cowsay, and our rule for updates is ^1.3.1 , which for the npm versioning rules means that npm can update to patch and minor releases: 0.13.1 , 0.14.0 and so on.

If there is a new minor or patch release and we type npm update , the installed version is updated, and the package-lock.json file diligently filled with the new version.

package.json remains unchanged.

To discover new releases of the packages, you run npm outdated .

Here's the list of a few outdated packages in one repository I didn't update for quite a while:

image-20210818164250791
image-20210818164250791

Some of those updates are major releases. Running npm update won't update the version of those. Major releases are never updated in this way because they (by definition) introduce breaking changes, and npm want to save you trouble.

To update to a new major version all the packages, install the npm-check-updates package globally:

npm install -g npm-check-updates

then run it:

ncu -u

this will upgrade all the version hints in the package.json file, to dependencies and devDependencies , so npm can install the new major version.

You are now ready to run the update:

npm update

If you just downloaded the project without the node_modules dependencies and you want to install the shiny new versions first, just run

npm install

 

 

Semantic versioning rules

Semantic Versioning is a convention used to provide a meaning to versions

If there's one great thing in Node.js packages, is that all agreed on using Semantic Versioning for their version numbering.

The Semantic Versioning concept is simple: all versions have 3 digits:x.y.z.

  • the first digit is the major version
  • the second digit is the minor version
  • the third digit is the patch version

When you make a new release, you don't just up a number as you please, but you have rules:

  • you up the major version when you make incompatible API changes
  • you up the minor version when you add functionality in a backward-compatible manner

The convention is adopted all across programming languages, and it is very important that every npm package adheres to it, because the whole system depends on that.

Why is that so important?

Because npm set some rules we can use in the package.json file to choose which versions it can update our packages to, when we run npm update .

The rules use those symbols:


^

~:

>:

>=;

<=;

<:

=:

-:

||: 

Let's see those rules in detail:

^ : if you write ^0.13.0 when running npm update it can update to patch and minor releases: 0.13.1 , 0.14.0 and so on.

~ : if you write ~0.13.0 , when running npm update it can update to patch releases:

0.13.1 is ok, but 0.14.0 is not.

> : you accept any version higher than the one you specify

>= : you accept any version equal to or higher than the one you specify

<= : you accept any version equal or lower to the one you specify

< : you accept any version lower to the one you specify

= : you accept that exact version

- : you accept a range of versions. Example: 2.1.0 - 2.6.2

|| : you combine sets. Example: < 2.1 || > 2.6

You can combine some of those notations, for example use 1.0.0 || >=1.1.0 <1.2.0 to either

use 1.0.0 or one release from 1.1.0 up, but lower than 1.2.0.

There are other rules, too:

no symbol: you accept only that specific version you specify ( 1.2.1 )

latest : you want to use the latest version available

 

https://fathomtech.io/blog/understanding-peer-dependencies-in-npm/

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

 

 


 



Mistake 8: manually adding dependencies to package.json

Don’t manually add dependencies to your package.json, please! You suck at alphabetizing, and you’re always forgetting to updatenpm-shrinkwrap.json. Use npm i --save and npm i --save-dev only; this will automatically update both package.json and npm-shrinkwrap.json(including doing any deduping). If there is a conflict in generating the shrinkwrap file, you’ll receive warnings about any version conflicts you need to resolve.

That being said, there are some quirks and bugs that make npm CLI maintenance of your package.json annoying. One big annoyance for me is in how versions are saved in package.json; I expect any @ version patterns to be reflected in package.json when doing an install — save, but that’s not what happens. Even if I do npm i — save package@~1.0.0, the version is saved with a ^ in package.json.

There’s also a bug relating to shrinkwrap files. If npm install --save detects that your dependency is already present in your shrinkwrap file, it erroneously stores the version in package.json with the resolved URL of the dependency, not the version number. (update, this has been fixed in the latest npm@5)

Still, despite these annoyances, it still makes sense to use the CLI, but make sure to double-check your package.json before committing it.

 


When a dependency is listed in a package as a peerDependency, it is not automatically installed. Instead, the code that includes the package must include it as its dependency.

npm will warn you if you run npm install and it does not find this dependency.

Example: let’s say package a includes dependency b:

a/package.json
{
  //...
  "dependencies": {
    "b": "1.x"
  }
}

Package b in turn wants package c as a peerDependency:

b/package.json
{
  //...
  "peerDependencies": {
    "c": "1.x"
  }
}

In package A, we must therefore add c as a dependency, otherwise when you install package b, npm will give you a warning (and the code will likely fail at runtime):

a/package.json
{
  //...
  "dependencies": {
    "b": "1.x",
    "c": "1.x"
  }
}

The versions must be compatible, so if a peerDependency is listed as 2.x, you can’t install 1.x or another version. It all follows semantic versioning.

Download my free Node.js Handbook and check out my courses!

 


 

 

Common NPM Mistakes

https://medium.com/@jacob.h.page/common-npm-mistakes-51bf8989079f

 

One of the best things about node development is npm. The large library of packages and ease of installing and publishing makes node an open source practitioner’s dream.

One of the worst things about node development is also npm. Actually, most of the problem is not with npm, but challenges in dealing with dependencies. I believe that many of these challenges stem from widespread misconceptions and poor practices.

Having personally built many large scale node applications  services, with big developer teams, I have seen firsthand many “dependency hell” struggles. I’d like to share the common mistakes I’ve observed with the hope that it can help you avoid the same pitfalls we encountered.

Mistake 1: using the wrong dependency type

There are so many dependency types within node that it’s a common mistake for the wrong one to be used. In general, you should determine the relationship of the dependency with your module and (if applicable) the consumer of your module. Eventually, I’d like to get around to authoring a more sophisticated decision flow chart around this, but here’s an abbreviated version.

devDependencies

Is the dependency used during the runtime of the application? If the answer to that question is “no,” then this is a dev dependency. Common categories of dev dependencies include tools for testing, building, minifying, bundling, transpiling, and linting. Sometimes this can seem ambiguous. What about projects using babel or webpack? The output from those tools is used at runtime, but those libraries are not. Some build tools also inject references to runtime modules, and those should not be dev dependencies. For example, babel-polyfill is a regular dependency, not a devDependency.

Be careful if you’re authoring a dev tool, like a gulp plugin or webpack loader. Although your module will be a dev dependency of an application, your own dependencies are not necessarily also dev dependencies themselves. If your build tool’s runtime functionality requires the use of a dependency, that is a regular dependency. But a build tool for your build tool is definitely a dev dependency.

peerDependencies

Is your module a plugin for some library or framework? Then that library/framework is a peer dependency. The consuming application is responsible for selecting and integrating a set of mutually-compatible plugins; the plugins themselves should not introduce said framework directly, but should instead specify a maximally-inclusive version range to make the job of integration as easy as possible. For example:

  • Your module is an express middleware; if it requires request and response properties only available in version 4, you should have express@^4.0.0as a peer dependency.
  • Your module provides gulp tasks or plugins. The application using your module obviously “owns” the gulp dependency, so your module should never provide its own gulp dependency.

optionalDependencies

Is your dependency something that is not supported on all platforms? And does your module also handle the case where the dependency is not installed? If the answer to both of those is “yes,” then this is an optional dependency. Do not use this dependency type if:

  • You want to allow the application to decide whether this is installed or not; although “optional” sounds like it would enable this scenario, there is currently no support for this. Sometimes peer dependencies are used as a workaround due to missing peer dependencies only raising warnings, but that is semantically incorrect and causes shrinkwrap to fail.
  • Your module cannot function without the dependency. If it cannot, the dependency is not optional. Consider using the engines property in your package.json to specify which environments your module is compatible with.

To put it another way, the only thing that optionalDependencies is good for is enabling an install to not fail if that dependency does not successfully install.

bundledDependencies

Personally, I have no exposure to these types of dependencies and have no guidelines for these.

dependencies (regular)

The remainder of dependencies are normal dependencies.

Mistake 2: avoiding peerDependencies

There’s a common misconception that peerDependencies in npm are “broken.” They aren’t broken! There are differences between how peerDependencies worked in v2 and v3, so if you’re expecting the behavior to stay the same, you may be forgiven for calling the new behavior “broken.” But I argue that npm 2 had the worse implementation.

With npm 2, if your package.json listed a peer dependency, when someone installed your package, that dependency was also automatically installed. This is definitely convenient in many ways, but it also caused other issues. See the GitHub issue for the behavior change for more details on this, but the gist of the problem is that it’s better to manually resolve peer dependency conflicts issues to avoid the accidental duplication that can occur during automatic installation. Hence npm 3 changed installation to warn you of missing peer dependencies instead, you can can explicitly add that dependency to your own package.json.

There’s also a misconception that peerDependencies is no-longer necessary because npm 3 introduced automatic de-duping. This is not correct! Its true that avoiding multiple installs of a package is one of the purposes of peerDependencies, but dependencies are only de-duplicated if the versions are all compatible. If they are not compatible, you’ll still get multiple versions installed.

To illustrate this, imagine if you had two express middleware dependencies:

{
 “name”: “my-app”,
 “dependencies”: {
   “middleware-a”: “*”,
   “middleware-b”: “*”
 }
}

Now suppose both of these list express as a dependency:

{
 “name”: “middleware-a”,
 “dependencies”: {
   “express”: “^3.0.0”
 }
}{
 “name”: “middleware-b”,
 “dependencies”: {
   “express”: “^4.0.0”
 }
}

Now what do you get if you require(‘express’)? Will you get version 3 or 4? Obviously, whether your file structure looks like this:

node_modules
 express@4
 middleware-a
   express@3
 middleware-b

…or this:

node_modules
 express@3
 middleware-a
 middleware-b
   express@4

…either way it’s broken and may not work as expected. In contrast, if your dependencies look like this:

{
 “name”: “middleware-a”,
 “peerDependencies”: {
   “express”: “^3.0.0”
 }
}{
 “name”: “middleware-b”,
 “peerDependencies”: {
   “express”: “^4.0.0”
 }
}

…when you install, you’ll be told that one requires express@3 and the other requires express@4. You’ll have to choose which express version your app will use and select different dependencies that are mutually compatible.

Mistake 3: not using shrinkwrap

If you are writing a module that is not itself an app/service, not using shrinkwrap is fine. However, I frequently hear this phrase from node developers that are authoring apps:

“Shrinkwrapping is annoying. We just use semver, so it’s completely unnecessary.”

This is wrongheaded! Semver versions simply convey developer intentions. If a developer intended a version to be backwards compatible, that’s not the same thing as it actually being backwards-compatible. Even if the package author correctly follows semver guidelines, your app may be relying on “buggy” behavior, and a fix for that bug, even in a patch revision bump, may break your app.

And of course, every release brings with it potential new bugs. Semver does not tell you anything about the quality of a release.

Time and time again, we dealt with breakage of our applications due to unsupervised version bumps. We learned that you cannot trust other developers to not break your app, and it’s not possible to have a test suite that will catch everything.

Shrinkwrap lets you determine exactly which versions of package you can expect to be installed. This way, if you are just updating your own code, you don’t shoulder the burden of also testing dependency upgrades you weren’t expecting.

Mistake 4: not shrinkwrapping devDependencies

If you use a CI/CD system like we do, you’ll save yourself a lot of grief if you shrinkwrap all dependencies, including devDependencies. Do npm shrinkwrap --dev every time. Otherwise your build servers will use different versions of build/test tools than yours, and you’ll get unexpectedly failing builds. And it’s not just the build tools you need to be concerned about, but the output of those build tools. For example, new babel plugin versions may cause unanticipated breakage in client JS.

With npm@4, dev dependencies are shrinkwrapped by default, which is a really good thing.

Mistake 5: ignoring peerDependency warnings

Your dependencies list their peerDependencies for a reason; they establish their compatibility. If you simply ignore peerDependency warnings, you’re ignoring what the package author told you would work, at your own peril.

Additionally, peer dependency warnings prevent shrinkwrap files from being written; this is a good thing, because you don’t want to shrinkwrap an install that is broken anyway.

Mistake 6: being overly-specific with peerDependencies

If you’re authoring a module that has peer dependencies, be as loose as possible with your version requirement; don’t just blindly use the latest version of those dependencies. Remember that the consuming application may be reconciling multiple version requirements; the less specific you are, the better.

Mistake 7: shrinkwrapping optional dependencies

If you’re working in a shop with multiple development environments, beware of platform-specific dependencies. Do not shrinkwrap fsevents, for example, if you expect an install to succeed on a Windows or Linux machine. You can avoid shrinkwrap including your dependency, even if it’s a sub-dependency, by listing it under your own package’s optionalDependencies.

Mistake 8: manually adding dependencies to package.json

Don’t manually add dependencies to your package.json, please! You suck at alphabetizing, and you’re always forgetting to updatenpm-shrinkwrap.json. Use npm i --save and npm i --save-dev only; this will automatically update both package.json and npm-shrinkwrap.json(including doing any deduping). If there is a conflict in generating the shrinkwrap file, you’ll receive warnings about any version conflicts you need to resolve.

That being said, there are some quirks and bugs that make npm CLI maintenance of your package.json annoying. One big annoyance for me is in how versions are saved in package.json; I expect any @ version patterns to be reflected in package.json when doing an install — save, but that’s not what happens. Even if I do npm i — save package@~1.0.0, the version is saved with a ^ in package.json.

There’s also a bug relating to shrinkwrap files. If npm install --save detects that your dependency is already present in your shrinkwrap file, it erroneously stores the version in package.json with the resolved URL of the dependency, not the version number. (update, this has been fixed in the latest npm@5)

Still, despite these annoyances, it still makes sense to use the CLI, but make sure to double-check your package.json before committing it.

Mistake 9: upgrading for the sake of upgrading

One approach our team has taken for maintaining shrinkwrap files was to do rm -rf node_modules && npm i && npm shrinkwrap --dev after every dependency change. Do not do this! You’re going to not-only get the new dependency you wanted, but you’re also introducing version changes for every other dependency at the same time.

This is a potentially controversial opinion, but you do not need to always upgrade to the latest version of everything!. Version bumps may introduce some valuable bug fixes, but more-often-than-not, this is what happens:

  • Because your version uses ^, you receive new features (minor version bumps) that you aren’t actually using yet and therefore don’t need.
  • You receive a patch version bump that fixes a bug that didn’t impact your app in the first place or that your app relied on behaviorally.
  • You receive new bugs that your tests don’t catch. You now have to debug which of the many version bumps caused your issues.

It’s true that you may need to eventually upgrade your packages, but the YAGNI principle applies, even with version bumps; only upgrade once you actually need an upgrade. Treat every version bump with the same scrutiny as changes to your own code, and be sure to have integration tests to ensure that your dependency works as expected.

Summary

Make sure you understand how dependencies work with npm, or you may encounter the dependency hell that has plagued many of your fellow developers. And for your own sake, shrinkwrap to avoid the headache of random version upgrades and non-deterministic installation.

Do you have any other advice relating to npm dependencies? I’d love to hear your thoughts

 

 


https://indepth.dev/posts/1187/npm-peer-dependencies

Understanding when and why to use npm peerDependencies

In this article I hope to clarify what *npm Peer Dependencies* are and especially when you should use them. Peer Dependencies are listed in the *package.json file* in the peerDependencies object.

To get the most out of this article you should have at least an introductory understanding of npm.

Contents

In this article:

  1. We will *compare* exactly how Dependencies work versus Peer Dependencies.
  2. We will look at some *examples* of both Dependencies and Peer Dependencies.
  3. Then, we will examine how npm handles *version conflicts*.
  4. Finally, having the fundamentals solidly in our grasp, we will lay out an approach to *deciding when Peer Dependencies are appropriate*.

The Scenario

To keep it real, let’s assume you’re creating an Angular Library or even just a simple JavaScript file that exports some functions.

Your project relies on packages from the npm Registry. These packages are your project’s *dependencies*.

You want to create your own *npm package* from your project. So you use npm pack to generate an *npm package* from your project. You might even decide to publish it to the npm Registry.

Other teams will add your package as a dependency in their own projects. We use Dependencies and Peer Dependencies in package.json to tell these other projects what packages also need to be added for our package to work.

So, at their most basic level here is how Dependencies and Peer Dependencies work:

Dependencies

*Dependencies* are listed in the package.json file in a dependencies object.

When you add a package in dependencies, you are saying:

  • My code needs this package to run.
  • If this package doesn’t already exist in my *node_modules* directory, then add it automatically.
  • Furthermore, add the packages that are listed in the package’s dependencies. These packages are called *transitive dependencies*.

Peer Dependencies

*Peer Dependencies* are listed in the package.json file in a peerDependencies object.

By adding a package in peerDependencies you are saying:

  • My code is compatible with this version of the package.
  • If this package already exists in *node_modules*, do nothing.
  • If this package doesn’t already exist in the *node_modules* directory or it is the wrong version, don’t add it. But, show a warning to the user that it wasn’t found.

Adding Dependencies

So, we add dependencies in the *package.json file* of our npm package folder. Let’s look at exactly how we add packages as dependencies and some examples of package dependencies.

Adding a Dependency

A *Dependency* is an npm package that our package depends on in order to be able to run. Some popular packages that are typically added as dependencies are lodash, request, and moment.

We add a regular dependency like this:

<>npm install lodash

npm adds the package name and version to the dependencies object in our project’s *package.json* file.

<>"dependencies": {
    "lodash": "^4.17.11"
  }

Some of you might remember the old days when we had to use the --save flag to get npm to update the dependencies in *package.json*. Thankfully, we don’t need to do that anymore.

Adding a Peer Dependency

*Peer Dependencies* are used to specify that our package is compatible with a specific version of an npm package. Good examples are Angular and React.

To add a *Peer Dependency* you actually need to manually modify your *package.json* file. For example, for Angular component library projects, I recommend adding angular/core as a peer dependency. So if you wanted to specify that your package is built for Angular 7, you could include something like this:

<>"peerDependencies": {
  "@angular/core": "^7.0.0"
}

About Conflicts

I get a lot of questions about whether a certain npm package should go into dependencies or into peerDependencies. The key to making this decision involves understanding how *npm* deals with *version conflicts*.

If you have read my previous articles, you know I like you to be able to do this stuff along with me! So feel free to work along with me for this little npm experiment.

 

conflict-test Project

To get started let’s create a trivial test project. I am going to name mine:
conflict-test

I created it like this:

<>md conflict-test
cd conflict-test
npm init -y

I then manually edited the *package.json* file and added two dependencies:

<>"dependencies": {
    "todd-a": "^1.0.0",
    "todd-b": "^1.0.0"
  }

These todd-a and todd-b packages also have their own dependencies:

todd-a

<>"dependencies": {
    "lodash": "^4.17.11",
    "todd-child": "^1.0.0"
  }

todd-b

<>"dependencies": {
    "lodash": "^4.17.11",
    "todd-child": "^2.0.0"
  }

The thing I want you to notice here is that todd-a and todd-b use the same version of lodash. But, they have a version conflict for todd-child:
todd-a uses todd-child *version 1.0.0*
todd-b uses todd-child *version 2.0.0*

Now I know that, like me, you are keenly interested to see how npm handles this version conflict. In my main project conflict-test I run npm install. As we would expect, npm magically installs the todd-a and todd-b packages in our *node_modules* folder. It also adds the packages that they depend on (the transitive dependencies). So after running npm install we take a look at the *node_modules* folder. It looks like this:

<>node_modules
├── lodash 4.17.11
├── todd-a 1.0.0
├── todd-b 1.0.0
│   └── node_modules
│       └── todd-child 2.0.0
└── todd-child 1.0.0

The interesting thing about this is that our project has one copy of lodash. But, it has two copies of todd-child. Notice that todd-b gets its own private copy of todd-child 2.0.0.

So here is the rule:

npm deals with version conflicts by adding duplicate private versions of the conflicted package.

An Approach to Peer Dependencies

As we saw from our experiment with npm version conflicts, if you add a package to your dependencies, there is a chance it may end up being duplicated in *node_modules*.

Sometimes, having two versions of the same package is fine. However, some packages will cause conflicts when there are two different versions of them in the same code base.

For example, assume our component library was created using *Angular 5*. We wouldn’t want our package adding another completely different version of angular/core when someone adds it as a dependency to their *Angular 6* application.

The key is:
We don’t want our library adding another version of a package to *node-modules* when that package could conflict with an existing version and cause problems.

peerDependencies or dependencies?

So this brings us to the main question for our dependencies:

When my package depends on another package, should I put it in dependencies or peerDependencies?

Well, as with most technical questions: it depends.

*Peer Dependencies* express compatibility. For example, you will want to be specific about which version of Angular your library is compatible with.

The Guidelines

Favor using Peer Dependencies when one of the following is true:

  • Having multiple copies of a package would cause conflicts
  • The dependency is visible in your interface
  • You want the developer to decide which version to install

Let’s take the example of angular/core. Obviously, if you are creating an Angular Library, angular/core is going to be a very visible part of your library’s interface. Hence, it belongs in your peerDependencies.

However, maybe your library uses Moment.js internally to process some time related inputs. Moment.js most likely won’t be exposed in the interface of your Angular Services or Components. Hence, it belongs in your dependencies.

Angular as a Dependency

Given that you are going to specify in your documentation that your library is a set of Angular Components and Services, you may be asking the question:

“Do I even need to specify angular/core as a dependency? If someone is using my library, they will already have an existing Angular project.”

Good question!

Yes, we can usually assume that for our Angular specific library the Workspace will already have the Angular packages available. Hence, technically we wouldn’t need to bother adding them to our list of dependencies.

However, we really do want to tell the developer *which Angular versions* our library is compatible with. So I recommend the following approach:

Add at least angular/core for the compatible Angular version to your peerDependencies.

This way developers will see a warning if they try to use your Angular 7 library in their Angular 6 project. Don’t bother adding the other Angular packages. You can assume if they have angular/core, they have the other Angular libraries.

In Conclusion

When in doubt you should probably lean toward using peerDependencies. This lets the users of your package make their own choice about which packages to add.

 

 

 

 

 


 

https://nodejs.org/es/blog/npm/peer-dependencies/

Peer Dependencies

por Domenic Denicola, 2013-02-08

Reposted from Domenic's blog with permission. Thanks!

npm is awesome as a package manager. In particular, it handles sub-dependencies very well: if my package depends on request version 2 and some-other-library, but some-other-library depends on request version 1, the resulting dependency graph looks like:

├── request@2.12.0
└─┬ some-other-library@1.2.3
  └── request@1.9.9

This is, generally, great: now some-other-library has its own copy of request v1 that it can use, while not interfering with my package's v2 copy. Everyone's code works!

The Problem: Plugins

There's one use case where this falls down, however: plugins. A plugin package is meant to be used with another "host" package, even though it does not always directly use the host package. There are many examples of this pattern in the Node.js package ecosystem already:

Even if you're not familiar with any of those use cases, surely you recall "jQuery plugins" from back when you were a client-side developer: little <script>s you would drop into your page that would attach things to jQuery.prototype for your later convenience.

In essence, plugins are designed to be used with host packages. But more importantly, they're designed to be used with particular versions of host packages. For example, versions 1.x and 2.x of my chai-as-promised plugin work with chai version 0.5, whereas versions 3.x work with chai 1.x. Or, in the faster-paced and less-semver–friendly world of Grunt plugins, version 0.3.1 of grunt-contrib-stylus works with grunt 0.4.0rc4, but breaks when used with grunt 0.4.0rc5 due to removed APIs.

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.

Even for plugins that do have such direct dependencies, probably due to the host package supplying utility APIs, specifying the dependency in the plugin's package.json would result in a dependency tree with multiple copies of the host package—not what you want. For example, let's pretend that winston-mail 0.2.3 specified "winston": "0.5.x" in its "dependencies" hash, since that's the latest version it was tested against. As an app developer, you want the latest and greatest stuff, so you look up the latest versions of winston and of winston-mail, putting them in your package.json as

{
  "dependencies": {
    "winston": "0.6.2",
    "winston-mail": "0.2.3"
  }
}

But now, running npm install results in the unexpected dependency graph of

├── winston@0.6.2
└─┬ winston-mail@0.2.3
  └── winston@0.5.11

I'll leave the subtle failures that come from the plugin using a different Winston API than the main application to your imagination.

The Solution: Peer Dependencies

What we need is a way of expressing these "dependencies" between plugins and their host package. Some way of saying, "I only work when plugged in to version 1.2.x of my host package, so if you install me, be sure that it's alongside a compatible host." We call this relationship a peer dependency.

The peer dependency idea has been kicked around for literally years. After volunteering to get this done "over the weekend" nine months ago, I finally found a free weekend, and now peer dependencies are in npm!

Specifically, they were introduced in a rudimentary form in npm 1.2.0, and refined over the next few releases into something I'm actually happy with. Today Isaac packaged up npm 1.2.10 into Node.js 0.8.19, so if you've installed the latest version of Node, you should be ready to use peer dependencies!

As proof, I present you the results of trying to install jitsu 0.11.6 with npm 1.2.10:

npm ERR! peerinvalid The package flatiron does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer flatiron-cli-config@0.1.3 wants flatiron@~0.1.9
npm ERR! peerinvalid Peer flatiron-cli-users@0.1.4 wants flatiron@~0.3.0

As you can see, jitsu depends on two Flatiron-related packages, which themselves peer-depend on conflicting versions of Flatiron. Good thing npm was around to help us figure out this conflict, so it could be fixed in version 0.11.7!

Using Peer Dependencies

Peer dependencies are pretty simple to use. When writing a plugin, figure out what version of the host package you peer-depend on, and add it to your package.json:

{
  "name": "chai-as-promised",
  "peerDependencies": {
    "chai": "1.x"
  }
}

Now, when installing chai-as-promised, the chai package will come along with it. And if later you try to install another Chai plugin that only works with 0.x versions of Chai, you'll get an error. Nice!

UPDATE: npm versions 1, 2, and 7 will automatically install peerDependencies if they are not explicitly depended upon higher in the dependency tree. For npm versions 3 through 6, you will receive a warning that the peerDependency is not installed instead.

One piece of advice: peer dependency requirements, unlike those for regular dependencies, should be lenient. You should not lock your peer dependencies down to specific patch versions. It would be really annoying if one Chai plugin peer-depended on Chai 1.4.1, while another depended on Chai 1.5.0, simply because the authors were lazy and didn't spend the time figuring out the actual minimum version of Chai they are compatible with.

The best way to determine what your peer dependency requirements should be is to actually follow semver. Assume that only changes in the host package's major version will break your plugin. Thus, if you've worked with every 1.x version of the host package, use "~1.0" or "1.x" to express this. If you depend on features introduced in 1.5.2, use ">= 1.5.2 < 2".

Now go forth, and peer depend!

Scroll to Top