Understanding npm Versioning
Tags: npm, node, javascript, typescript
Published: 2023-04-04 15:00:00

In the world of software development with JavaScript and TypeScript, it is crucial to manage the **versions of packages** used in your projects. This blog post will show the details of npm package management, specifically focusing on the **caret** (`^`) and **tilde** (`~`) symbols, as well as other versioning notations. Get ready to better understand how to manage your project's dependencies and establish reproducible builds with consistency in package versions!
<!--more-->
## Major, minor, patch
The npm ecosystem uses [semantic versioning](https://semver.org/) where version numbers typically consist of three parts, separated by dots: `major.minor.patch`. These parts represent different types of changes made to the software, and are used to help developers and users understand the significance of a given version number.

### Major version 

The major version number is typically incremented when there are breaking changes to the software. It indicates that the software is no longer backwards compatible with previous versions, and users may need to make changes to their code in order to upgrade. 

For instance, API contracts may change if parameters are modified:

**Version 1.0.0**

```ts
export function add(a: number, b: number): number {
  return a + b;
}
```

**Version 2.0.0**

The API contract for Version 1.0.0 has been violated due to changes in the input parameter types for `a` and `b`. As a result, Version 2.0.0 must be introduced to address this breaking change:

```ts
export function add(a: string, b: string): number {
  return parseInt(a, 10) + parseInt(b, 10);
}
```

**Important:** Following the [npm guidelines](https://docs.npmjs.com/cli/v6/using-npm/semver#caret-ranges-123-025-004), you can make breaking changes in your APIs as long as you are below version 1.0.0. When your package is in major version zero (e.g., `0.x.x`), the semver rules change slightly. In this case, breaking changes occur in `0.x.x` (minor-level), while new features and patches are implemented in `0.0.x` (patch-level).

### Minor version

The minor version number is typically incremented when new features are added to the software, or when there are significant enhancements or improvements to existing features. These changes are usually backwards compatible, meaning that users can upgrade to the new version without having to make major changes to their code.

**Version 1.0.0**

```ts
export function add(a: number, b: number): number {
  return a + b;
}
```

**Version 1.1.0**

The `add` function has been updated to accept an indefinite amount of numbers, improving on its original implementation. This update does not affect prior API calls, as the function can still be used by providing just two numeric arguments, maintaining backwards compatibility.

In addition to the existing `add` function, a new `subtract` function has been added. It requires a minor release (`1.1.0`) to introduce these new features:

```ts
export function add(...numbers: number[]): number {
  return numbers.reduce((sum, current) => sum + current, 0);
}

export function subtract(a: number, b: number): number {
  return a - b;
}
```

### Patch version

The patch version number is typically incremented when bugs or security issues are fixed in the software. These changes are generally small, and do not involve major changes to the functionality or features of the software. 

Let's assume we updated the `subtract` function from version 1.1.0 to 1.2.0, adding the ability to also accept an indefinite amount of numbers as input:

**Version 1.2.0**

```ts
export function subtract(...numbers: number[]): number {
  return numbers.reduce((sum, current) => sum - current, 0);
}
```

Suppose we've discovered that using `0` as the initial value for the subtract function was a mistake. Rather than subtracting from `0`, we want the function to subtract from the first number that is given as input. As a result, we'll need to release a patch version (`1.2.1`) to fix this issue:

**Version 1.2.1**

```ts
export function subtract(...numbers: number[]): number {
  return numbers.reduce((sum, current) => sum - current);
}
```

## Understanding Caret (`^`) and Tilde (`~`)

When specifying dependencies in our `package.json` file, the caret and tilde symbols have special significance in determining the range of acceptable package versions.

### Video Tutorial

{% youtube 3XKIs1_FpLw %}

### Caret (`^`)

The caret symbol indicates that npm should restrict upgrades to **patch or minor level updates**, without allowing major version updates. For example, given `"^5.0.2"`, npm will permit updates within the same major version (e.g., `5.1.0`, `5.0.3`), but not a jump to a new major version (e.g., `6.0.0`) when running `npm update`.

### Tilde (`~`)

By altering the caret symbol to a tilde symbol, we would only receive updates at the **patch level**. If we were to use `"~5.0.2"`, we would obtain version `5.0.3` if it were available, but not `5.1.0`.

### Trick to remember

A helpful way to remember the difference between these symbols is by envisioning a house: the tilde is the floor, while the caret represents the rooftop, enabling you to reach higher version numbers:

![Caret and Tilde](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j4uequ96jaff7cm5a4s2.PNG)

## Alternative Versioning Notations

If you're not concerned with specifying the precise minimum version and simply want to obtain the most recent version in a certain range, you can use alternative notations.

### Latest Patch Version

To get the latest **patch version**, use the notation `5.0.x`. This will install the most recent version with the given major and minor numbers (e.g., `5.0.6`).

### Latest Minor and Patch Versions

Similarly, to obtain the latest **minor version**, along with its most **recent patch**, use the notation `5.x.x`. This will permit updates within the same major version, but without restrictions on the minor version (e.g., `5.1.3` or `5.2.0`).

## Updating Dependencies

It's essential to properly manage your dependency updates when changing version numbers in your `package.json` file.

### npm install

Executing the `npm install` command installs a package's dependencies and any dependencies that these dependencies rely on (transitive dependencies). It also creates a `package-lock.json` file if it doesn't exist.

**Important:** It is worth mentioning that `npm install` will download _compatible versions_ when there is no `package-lock.json` file present. This means that the version you receive may not be the latest minor version, even if you've specified a version range using the caret symbol.

**Example**

If you've included `typescript@^4.8.3` in the `devDependencies` section of your `package.json` file, you may expect to receive the latest minor version (currently `4.9.5`) when you run `npm install`. However, you will receive the latest patch version (`4.8.4`) as it is compatible with your version range. In order to obtain `4.9.5`, you will need to execute `npm update`.

Dependency | npm install | npm update
:---:|:---:|:---
`typescript@~4.8.3` | `4.8.4` | `4.8.4`
`typescript@^4.8.3` | `4.8.4` | `4.9.5`
`typescript@4.8.X` | `4.8.4` | `4.8.4`
`typescript@4.X.X` | `4.9.5` | `4.9.5`

### npm update

To apply changes to the version numbers in your `package.json`, use the `npm update` command. Unlike `npm install`, which only downloads a compatible version, `npm update` will also update the package to the latest version that matches the specified range. Additionally, the `package-lock.json` file will be updated.

### Recording Exact Versions

You might wonder: "How can I determine which version was actually downloaded?" That's where the `package-lock.json` file comes in. This file records the exact version retrieved during the initial installation, or any subsequent updates.

With a `package-lock.json` file in place, future runs of `npm install` will download the same build, ensuring **reproducible results** for your project's dependencies. This is particularly important in collaborative development environments, where multiple team members work on the same project, as it guarantees consistency across all installations.

### Traffic Light Control

If you use the [Yarn Package Manager](https://yarnpkg.com/) instead of [npm](https://www.npmjs.com/) and run the command `yarn upgrade-interactive --latest`, you'll see a color-coded legend that indicates the likelihood of a package update breaking your code:

- **Patch updates** are indicated in **green**, as they typically include backward-compatible fixes and pose the least risk of breaking your application
- **Minor updates** are indicated in **yellow**, as they typically involve more changes than patch updates and therefore carry a higher risk of causing issues if their package maintainers don't strictly adhere to semantic versioning guidelines
- **Major updates** are indicated in **red**, as they often include backward-incompatible changes that may require you to update your code in order to maintain compatibility

**Screenshot**

![yarn upgrade-interactive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nup5tjvx86sg54rpe1o4.png)

## Conventional Commits

As we've learned, major version updates should only be issued when there are breaking changes. To draw attention to a breaking change, the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#commit-message-with--to-draw-attention-to-breaking-change) suggests using an exclamation mark (`!`) after the **type** or **scope** in a commit message.

**Example**

> refactor(api)!: use stringified numbers in calculations

Tools for publishing, such as [Lerna](https://lerna.js.org/) (when using the `--conventional-commit` flag), follow this convention when incrementing package versions and generating changelog files.

## Publishing packages

Publishing packages with npm is a crucial step in sharing your code with the wider community. The npm CLI provides commands (`npm version` and `npm publish`) to assist with proper versioning and release of your own packages, in accordance with semantic versioning rules.

**Video**

{% youtube kK4Meix58R4 %}

### npm version

The [npm version](https://docs.npmjs.com/cli/v9/commands/npm-version) command is used to update the version of a package in the `package.json` file. If run in a Git repository, it will generate a Git tag and a new commit with a message that includes the version number.

### npm publish

The [npm publish](https://docs.npmjs.com/cli/v9/commands/npm-publish) command is used to publish the package to the registry, making it available for installation by others.

### Release Workflow

The `npm version` and `npm publish` commands can be used to set up release workflows within the scripts section of a `package.json` file:

```json
{
  "scripts": {
    "preversion": "git checkout main && git pull && npm install && npm test && npm run build",
    "release:major": "npm version major",
    "release:minor": "npm version minor",
    "release:patch": "npm version patch",
    "postversion": "git push origin && git push origin --tags && npm publish --access public"
  }
}
```

## Conclusion

In this blog post, we've explored the significance of the caret and tilde symbols in npm versioning, as well as alternative versioning notations. We've also looked at how to use the `npm update` command and the importance of the `package-lock.json` file.

With a better understanding of npm package management and the tools available to manage dependency versions, you can ensure a stable, consistent, and reproducible development environment for your projects. Happy coding!