Working with Package Versions

7 min read
npmpackage managerversions

Sample Image

Versions

When working with a package manager, you've probably noticed version numbers like 1.2.0, 2.5.6, and so on. These numbers consist of three parts: the major version, the minor version, and the so-called patch version.

But what do they mean? Let's figure it out! 🚀

npm, yarn, and other package managers use Semantic Versioning (SemVer), which looks like X.Y.Z (Major.Minor.Patch).

  • Major: Breaking changes (2.0.0) that may break backward compatibility. If you update to a major version, code that previously worked might stop working, so functionality must be tested after the update.

  • Minor: New features compatible with older ones (1.3.0), which do not break backward compatibility. This means that after updating, your code should continue to work just as it did before.

  • Patch: Bug fixes (1.2.4) that do not break backward compatibility. We update - and everything works.

BUT! I recommend performing a regression test of your functionality after any library update regardless.

Suppose we have a package version 1.2.3. If the developer releases a new version numbered 2.0.0, it means significant changes were made to the package that may break backward compatibility. However, if the developer releases version 1.3.0, it indicates that new features were added to the package, but backward compatibility is preserved.

Version Management in package.json

In the package.json file, you specify your project's dependencies and their versions. Versions can be fixed exactly, or you can use symbols to specify valid version ranges.

  • Exact version: "react": "1.0.1" - version 1.0.1 will always be used.

  • Symbol ^: "react": "^1.0.1" - any MINOR or PATCH version starting from 1.0.1 will be used (e.g., 1.1.0 or 1.0.2).

  • Symbol ~: "react": "~1.0.1" - any PATCH version starting from 1.0.1 will be used (e.g., 1.0.2), but not 1.1.0.

Remember a simple rule:

^ = "I want new features, but don't break my code" (most common option).

~ = "Only bug fixes, don't change anything else".

How to Update Packages

(From here on, we will use npm)

Actually, before updating packages, you need to check which ones are already outdated. This can be done using the command:

npm outdated

You will see a table with columns Current (current installed), Wanted (maximum allowed by ^ or ~), and Latest (absolute latest in the registry).

PackageCurrentWantedLatestLocationDepended by
react17.0.217.0.218.2.0node_modules/reactmy-app
axios0.21.10.21.41.6.5node_modules/axiosmy-app
lodash4.17.154.17.214.17.21node_modules/lodashmy-app

Let's look at a few update options:

  1. Safe update To update packages to the Wanted version (i.e., taking into account your ^ and ~):

npm update

  • This command will update files in the node_modules folder.
  • It will also update the package-lock.json file.
  • Important: It will not change the versions written in your package.json if they still fall under the conditions (for example, if it says ^1.0.0 and you updated to 1.5.0, the text in the file will remain ^1.0.0, since 1.5.0 fits this rule).
  1. Full update If you want to update from version 1.x.x to 2.x.x (ignoring rules in package.json), the npm update command won't do it. For a single package, we can use the command:

npm install <package_name>@latest

This will forcefully install the latest version and rewrite package.json.

And to automatically update all packages, use the npm-check-updates utility. It checks for latest versions and overwrites your package.json. Run the check (without installing the utility):

npx npm-check-updates

If you want to apply changes to package.json, add the -u flag:

npx npm-check-updates -u

After that, be sure to install the new versions:

npm install

It all looks simple, but in practice problems may arise, so give it a try!

Why shouldn't you delete package-lock.json?

Actually, this file is a snapshot of your project's dependency tree, and it should not be deleted. I don't know why, but many people think it's just a copy of package.json. So why is it needed?

Imagine: you are working in a team, and your package.json has a dependency with version ^0.21.1. Today you installed the package, and version 0.21.1 was downloaded. But tomorrow comes, a frontend intern joins you and installs dependencies in the project. And - oh no! - he gets the same dependency but with version 0.21.2, because a new version was released. All because of the ^ symbol. The result: if developers accidentally introduced a bug in version 0.21.2, your project will work, but your colleague's won't. Even though your package.json says the same thing!

REMEMBER: package-lock.json fixes the exact versions (e.g., strictly 0.21.1) of all installed packages and their own sub-dependencies. When a colleague types npm install, the system will look at the lock file and install exactly the same dependencies as you have.

Main functions of package-lock.json:

  • Identity Guarantee: The same code will be installed on any computer and on the server (Production).

  • Security: The file stores integrity (hash sums). If someone hacks a package in the npm registry and substitutes the code for version 0.21.1, npm will notice the hash mismatch and produce an error.

  • Speed: npm doesn't need to calculate the dependency tree every time; it simply takes the ready-made scheme from the file.

Additional Operators

There are other operators, but I've seen them very rarely. Examples are provided in the table:

SymbolRuleExample
>Accept updates of any version higher than specified>0.13.0: 0.13.1, 0.14.1, 1.1.1
<Accept updates of any version lower than specified<3.0.0: 2.0.0, 2.9.0
>=Accept any version greater than or equal to specified>=3.0.0: 3.0.0, 4.1.0
<=Accept any version less than or equal to specified<=3.0.0: 3.0.0, 2.9.0
=Accept only the specified exact version=3.0.0: 3.0.0 (not 3.0.1)
-Accept a range of versions (inclusive)1.0.0 - 1.10.10: 1.5.0 (not 1.11.0)
||Combination of versions (logical OR)<2.1.0 || >2.6.0: 2.0.1, 3.1.0
&&Versions satisfying both conditions (logical AND)>=1.0.0 && <2.0.0: 1.5.0 (not 2.0.0)
(space)Implicit logical AND (same as &&)>=1.0.0 <2.0.0: 1.5.0 (not 2.0.0)
*Accept any version (wildcard)*: any version available
xAny version for this position1.x: 1.0.0, 1.5.0 (not 2.0.0)
XSame as x (case insensitive)1.X: 1.0.0, 1.5.0 (not 2.0.0)
(none)Accept only the specified exact version3.0.0: 3.0.0 (not 3.0.1)
latestAlways install the latest available versionnpm install <package>@latest
@tagInstall a specific distribution by tagnpm install react@beta

And a small example of package.json with such operators:

{
  "dependencies": {
    "express": "^4.18.0", // Allow minor updates: 4.18.x, 4.19.x
    "lodash": "~4.17.21", // Allow only patch updates: only 4.17.x
    "react": ">=18.0.0", // Any version 18.0.0 or higher
    "typescript": "4.x", // Any version within the major branch 4.x
    "eslint": "*", // Always install the very latest version
    "axios": ">=1.0.0 && <2.0.0", // Only versions of branch 1.x
    "my-utils": "workspace:^1.0.0", // Dependency from workspace (monorepo)
    "local-pkg": "file:../local-package" // Dependency from local folder
  },
  "devDependencies": {
    "jest": "29.0.0 - 29.5.0", // Specific version range
    "prettier": "2.8.8", // Exact version
    "@types/node": ">=16.0.0 <21.0.0" // Logical "AND" (space separated)
  }
}

Command Cheatsheet

CommandDescription
npm outdatedCheck for outdated packages
npm updateUpdate packages within semver ranges
npm install package@1.2.3Install a specific package version
npm install package@latestInstall the latest package version
npm install package@betaInstall a pre-release (beta) version
npm listShow the tree of installed package versions
npm view package versions --jsonShow all available package versions in JSON format

That's all, thanks for reading 🙏

Working with Package Versions | Frontend Tales