Semantic Versioning (semver) is a versioning scheme that is officially recommended by npm. It recommends every package to be versioned in the format MAJOR.MINOR.PATCH and to follow 3 basic principles of incrementing/bumping (Quoted from https://semver.org/):

  1. MAJOR version when you make incompatible API changes;
  2. MINOR version when you add functionality in a backward compatible manner;
  3. PATCH version when you make backward-compatible bug fixes.

Under the scheme of Semantic Versioning, how should we increment the version of a package after updating

  • a dependency/optional dependency as specified in dependencies/optionalDependencies,
  • a dev dependency as specified in devDependencies, or
  • a peer dependency as specified in peerDependencies?

For the impatient, feel free to jump to the conclusion.

Dependency / Optional Dependency

In most cases in the npm ecosystem, dependencies and optional dependencies can be seen as part of internal implementation. Thus, in most cases, merely bumping the version of a dependency or optional dependency can be seen as internal changes. Thus, unless the package also makes other relevant changes, merely bumping the version of a dependency usually should only lead to incrementing PATCH.

Exceptions

Like almost everything in software engineering, there are exceptions. The following is a couple of example situations in which we should increment MAJOR or MINOR:

Dev Dependency

A dev dependency, in most cases, does not impact runtime behaviors. Therefore, merely bumping the version of a dev dependency should not usually even bump the version of the package.

Exceptions

Sometimes, the updated dev dependency involves generating part of what is being packaged and an update leads to a changed package. Here is a couple of examples:

  • Consider a package that has the typescript package as a dev dependency. An update of the typescript package may lead to changed type declaration files.
  • Consider a package that has the vite package as a dev dependency. An update of the vite package may lead to an updated built package.

Peer Dependency

Patterns of Peer Dependencies

Before we delve into version bumping, let us review the common patterns of peer dependencies.

Consider Package A that specifies a range of versions of Package P as its peer dependency. Package P acts as a host of Package A, and Package A acts as a plugin to Package P. We can see Package P as an environment required by Package A. A dependent of Package A (referred to as Package U) also typically specifies Package P as a dependency/dev dependency. The figure below illustrates this relationship.

Peer Dependency Package Relationship

For example, an eslint plugin (Package A) typically specifies a range of eslint versions (Package P) as its peer dependency. These versions of eslint are the environment that the eslint plugin requires to run. A package (Package U) that uses this eslint plugin usually specifies both eslint and this eslint plugin as dev dependencies.

Versioning after Updating a Peer Dependency

With the pattern of package relationships around peer dependencies in mind, let us consider the consequence of updating a peer dependency of Package A from the perspective of Package U. Then, based on the consequence, we decide how to bump versions of Package A by following the semver principles at the beginning of this article. We also assume no other API changes to Package A and that Package P also follows semver.

Widening Acceptable Versions

If Package A widens the range of acceptable versions of Package P, e.g., from ^1.0.0 to ^1.0.0 || ^2.0.0, then we should bump MINOR since supporting more versions of Package P is a backward-compatible new feature.

Bumping MAJOR of a Peer Dependency

If Package A requires a later version of Package P with a MAJOR version increment, e.g., from ^1.0.0 to ^2.0.0, then we should bump MAJOR since the change requires Package U to update its dependency Package P to a backward-incompatible version.

Bumping MINOR or PATCH of a Peer Dependency

What should we do if Package A requires a later version of Package P with a MINOR or PATCH version increment, e.g., from ^1.0.0 to ^1.1.0 or from ^1.0.0 to ^1.0.1?

First, we should understand that this is not a backward-incompatible change in Package A, because Package U can update Package P as a dependency across MINOR and PATCH increments with no backward compatibility concerns.

Then, we need to figure out whether we should bump MINOR or PATCH of Package A. This requires understanding the rationale behind the principle of incrementing MINOR in semver. Besides conveying a message of new APIs or substantial improvements to human readers, this distinction also implies that it is unsafe to downgrade the dependency if the downgrade decrements MINOR, while it is safe if the downgrade decrements PATCH only.

With this understanding in mind, it is not hard to see that:

  • Consider the situation in which Package A requires a later version of Package P with a MINOR version increment, e.g., from ^1.0.0 to ^1.1.0. We should bump MINOR of Package A because it is unsafe for Package U to downgrade Package A beyond this version, which requires downgrading Package P beyond a MINOR version change.
  • Consider the situation in which Package A requires a later version of Package P with a PATCH version increment, e.g., from ^1.0.0 to ^1.0.1. We should bump PATCH of Package A because it is safe for Package U to downgrade Package A beyond this version, which only requires downgrading Package P with a PATCH version decrement.

Conclusion

Type of Updated DependencyAction
(Optional) DependencyBump PATCH, unless an exception applies
Dev DependencyDon't bump version, unless an exception applies
Peer DependencyIf Peer DependencyAction on the Package
Widen Version RangeBump MINOR
Bump MAJORBump MAJOR
Bump MINORBump MINOR
Bump PATCHBump PATCH