Skip to main content

Appendix B - Versioning with SemVer

Introduction to Versioning Principles

This guide explains how to manage module versions using Semantic Versioning (SemVer). It covers best practices for versioning modules, selecting compatible dependency versions, resolving conflicts, and provides practical examples to help avoid common pitfalls.

Semantic Versioning (SemVer) Overview

Semantic Versioning (SemVer) is a widely adopted versioning scheme that helps communicate the nature of changes in a module. The version format is always MAJOR.MINOR.PATCH, for example, 3.2.5, where:

  • PATCH: Backwards-compatible bug fixes.
  • MINOR: Backwards-compatible new features.
  • MAJOR: Changes that break backwards compatibility.

Special Considerations for Pre-1.0.0 Versions

For versions before 1.0.0, the MINOR version indicates breaking changes instead of the MAJOR version. Exercise caution when incrementing versions below 1.0.0.

warning

Do not prefix version numbers with "v". Use 3.2.5 instead of v3.2.5.

For more detailed explanations and examples, see the Versioning Examples section below.

Specifying Compatible Dependency Versions

When declaring dependencies in package.json—whether under dependencies, peerDependencies, or devDependencies—it is crucial to specify versions that ensure compatibility.

Using Version Ranges

Instead of a fixed version, use version ranges to allow flexibility while maintaining compatibility. The caret (^) symbol is commonly used for this purpose:

  • "^1.2.3" allows versions >= 1.2.3 and < 2.0.0 (e.g., 1.99.99 but not 2.0.0 or higher).
  • "^0.1.2" allows versions >= 0.1.2 and < 0.2.0.

Handling Different Versions of the Same Dependency

Different modules may specify different versions of the same dependency. During installation, npm resolves dependencies by selecting the highest version that satisfies all constraints:

  • If multiple versions satisfy all constraints, the highest (latest) version is used.
  • If no single version satisfies all constraints:
    • For dependencies, multiple versions of the module are installed. This should be avoided because it leads to multiple copies in node_modules and can cause transpilation conflicts.
    • For peerDependencies, npm throws an exception. You must resolve all peerDependencies to compatible versions before proceeding.

Best Practices in ADITO Modules

  • Use peerDependencies to prevent multiple installations of the same dependency.
  • Only project modules should use dependencies.
  • Avoid installing the same module multiple times to prevent runtime conflicts.

Temporary Version Overrides

For temporary testing, you can use the overrides field in your project’s package.json to force specific dependency versions. Refer to the npm documentation for details.

Updating Dependencies with npm

To update dependencies while ensuring consistent resolution, follow these steps:

npm update --package-lock-only
npm clean-install

Best Practices

  • Use ^x.y.z to allow non-breaking updates while maintaining compatibility.
  • Avoid overly broad version ranges such as * or >= unless absolutely necessary.
  • Pin critical shared libraries to fixed versions in production environments to ensure stability.

Examples and Scenarios

This section provides practical examples illustrating how to apply SemVer principles and manage peerDependency changes effectively.

Understanding Increments

In SemVer, the first non-zero segment in a version number indicates the level of change. Examples of valid version increments include:

  • 0.0.1 → 0.0.2 (patch)
  • 0.1.0 → 0.1.1 (patch)
  • 0.1.0 → 0.2.0 (minor)
  • 1.0.0 → 1.0.1 (patch)
  • 1.0.0 → 1.1.0 (minor)
  • 1.0.0 → 2.0.0 (major)

Versioning When Modifying peerDependencies

When updating a module’s peerDependencies, the version of the module itself must reflect the nature of the change.

Example: Correct Versioning After a Breaking Dependency Change

Before:

{
"name": "@aditosoftware/my-module",
"version": "1.0.0",
"peerDependencies": {
"@aditosoftware/example": "^1.0.0"
}
}

After (major version bump due to breaking change in peerDependency):

{
"name": "@aditosoftware/my-module",
"version": "2.0.0",
"peerDependencies": {
"@aditosoftware/example": "^2.0.0"
}
}

Example: Incorrect Versioning (Missing Major Bump)

{
"name": "@aditosoftware/my-module",
"version": "1.0.1",
"peerDependencies": {
"@aditosoftware/example": "^2.0.0"
}
}

This is incorrect because the peerDependency change introduces a breaking change that requires a major version increment.

Dependency Conflict Scenario in a Project

Consider a project with the following dependencies:

{
"name": "@aditosoftware/project",
"dependencies": {
"@aditosoftware/example": "^1.0.0",
"@aditosoftware/my-module": "^1.0.0"
}
}

If example is updated to 2.0.0 but my-module is only incremented with a minor or patch version, an error will occur during npm install due to conflicting versions:

project:
- example (^1.0.0):
-> example: 1.0.0
- my-module (^1.0.0):
-> my-module: 1.0.1
-> example: 2.0.0

This constitutes a breaking change and requires a major version update of my-module.

Post-Publish Fix: Correcting an Incorrect peerDependency Version

If a major version is published without correctly updating the peerDependency, a hotfix may be necessary.

Before:

{
"name": "@aditosoftware/my-module",
"version": "1.0.0",
"peerDependencies": {
"@aditosoftware/example": "^1.0.0"
}
}

After publishing version 2.0.0 without updating peerDependency:

{
"name": "@aditosoftware/my-module",
"version": "2.0.0",
"peerDependencies": {
"@aditosoftware/example": "^1.0.0"
}
}

Fix with a patch version updating the peerDependency:

{
"name": "@aditosoftware/my-module",
"version": "2.0.1",
"peerDependencies": {
"@aditosoftware/example": "^2.0.0"
}
}

Note: Applying such a hotfix is acceptable immediately after publishing but should be avoided once the version has been widely adopted.

The CHANGELOG.md should mention that version 2.0.0 is deprecated due to a missing peerDependency update and reference version 2.0.1.

Non-Breaking peerDependency Changes

Certain changes to peerDependencies do not break consumers and can be handled with patch version increments.

  • Removing a peerDependency

    Before:

    {
    "name": "@aditosoftware/my-module",
    "version": "1.0.0",
    "peerDependencies": {
    "@aditosoftware/example": "^1.0.0"
    }
    }

    After (patch version bump):

    {
    "name": "@aditosoftware/my-module",
    "version": "1.0.1",
    "peerDependencies": {
    // example removed
    }
    }
  • Extending the compatible version range

    Before:

    {
    "name": "@aditosoftware/my-module",
    "version": "1.0.0",
    "peerDependencies": {
    "@aditosoftware/example": "^1.0.0"
    }
    }

    After (patch version bump):

    {
    "name": "@aditosoftware/my-module",
    "version": "1.0.1",
    "peerDependencies": {
    "@aditosoftware/example": "^1.0.0 || ^2.0.0" // my-module 1.0.1 supports both major versions
    }
    }