DEV Community

Cover image for Understanding npm Versioning
Benny Code for TypeScript TV

Posted on • Edited on

Understanding npm Versioning

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!

Major, minor, patch

The npm ecosystem uses semantic versioning 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



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


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

Important: Following the npm guidelines, 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



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


Enter fullscreen mode Exit fullscreen mode

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:



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;
}


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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



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

Enter fullscreen mode Exit fullscreen mode




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

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

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 instead of npm 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

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 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 (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

npm version

The 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 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:



{
"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"
}
}
Enter fullscreen mode Exit fullscreen mode




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!

Top comments (5)

Collapse
 
pcreager23 profile image
pcreager23

Hi @bennycode ,
I found this article while trying to learn about how npm uses the @ symbol in package paths. I understand that the @ signifies TS, as opposed to JS code - is that documented anywhere I can use as a reference?

Thanks

Collapse
 
bennycode profile image
Benny Code

Usually the @ signals an organization. Where did you see it?

Collapse
 
pcreager23 profile image
pcreager23

Example:

https://www.npmjs.com/package/babel-traverse
https://www.npmjs.com/package/@babel/traverse
Enter fullscreen mode Exit fullscreen mode

I'm told the difference is the non-@ is JavaScript code, while the @ version is TypeScript code. That is what I want to verify, and ideally find any documentation that states this naming convention.

Thread Thread
 
bennycode profile image
Benny Code

That's a wrong assumption. Packages starting with an @ simply belong to an organization. Most communities (like Babel) create an organization now because that way you can be sure that the code (like "traverse") comes from them and not any other npm user. You can read more about organization scopes here: docs.npmjs.com/creating-an-organiz...

Collapse
 
ghostcode profile image
Zhuxy

thanks