Working with Package Versions

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).
| Package | Current | Wanted | Latest | Location | Depended by |
|---|---|---|---|---|---|
| react | 17.0.2 | 17.0.2 | 18.2.0 | node_modules/react | my-app |
| axios | 0.21.1 | 0.21.4 | 1.6.5 | node_modules/axios | my-app |
| lodash | 4.17.15 | 4.17.21 | 4.17.21 | node_modules/lodash | my-app |
Let's look at a few update options:
- 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).
- Full update
If you want to update from version 1.x.x to 2.x.x (ignoring rules in package.json), the
npm updatecommand 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:
| Symbol | Rule | Example |
|---|---|---|
> | 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 |
x | Any version for this position | 1.x: 1.0.0, 1.5.0 (not 2.0.0) |
X | Same as x (case insensitive) | 1.X: 1.0.0, 1.5.0 (not 2.0.0) |
(none) | Accept only the specified exact version | 3.0.0: 3.0.0 (not 3.0.1) |
latest | Always install the latest available version | npm install <package>@latest |
@tag | Install a specific distribution by tag | npm 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
| Command | Description |
|---|---|
npm outdated | Check for outdated packages |
npm update | Update packages within semver ranges |
npm install package@1.2.3 | Install a specific package version |
npm install package@latest | Install the latest package version |
npm install package@beta | Install a pre-release (beta) version |
npm list | Show the tree of installed package versions |
npm view package versions --json | Show all available package versions in JSON format |
That's all, thanks for reading 🙏