What is a version?
As stated in npm's documentation:
The name and version [of a package] together form an identifier that is assumed to be completely unique. Changes to the package should come along with changes to the version.
This "identifier" is usually presented as {name}@{version}
(as mentioned in Scoped packages), e.g. [email protected]
would correspond to a package file containing:
{
"name": "eslint",
"version": "8.48.0",
// ...
}
You may have noticed that these versions are generally in the format x.y.z
and seen additional characters like ^
and ~
popping up around them in your own package files. Understanding what this means is extremely important, especially when it comes to dealing with issues like conflicts and vulnerabilities.
Semantic versioning
Semantic versioning (or "semver") is a structured way of deciding how to change a package's version when a new version is released. x.y.z
means "major version x
, minor version y
and patch version z
".
Summary
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes
- MINOR version when you add functionality in a backward compatible manner
- PATCH version when you make backward compatible bug fixes
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
For example, if your package was starting at "version": "1.2.3"
, the next version would be:
1.2.4
if you were just fixing a bug (making no change to the public API);1.3.0
if you were adding a new feature (only adding to the public API); or2.0.0
if you were making more fundamental changes (e.g. removing from the public API).
Using these conventions makes it easier for consumers of packages to understand, for a given new release of the package, whether they should expect to need to make changes to their own package when upgrading. A major version change doesn't always guarantee that every consumer will have to make changes (for example if you drop support for a Node version that should be a major version bump, as I discuss here, but consumers already using compatible versions don't have to do anything) but gives consumers a signal that they should at least be checking what migration steps are required and checking for regressions in their own functionality.
Version ranges
The reflection of this on npm's side is specifying a range of valid versions for your dependencies. Imagine you have a package like this:
{
// ...
"dependencies": {
"pinned": "1.2.3",
"bugfix-only": "~2.3.4",
"new-features": "^3.4.5"
},
// ...
}
- With no additional characters, the version must be an exact match
[email protected]
is the only version that satisfies this requirement- Even a patch change to
[email protected]
would be invalid.
- The
~
(tilde) prefix means that the patch version can change.[email protected]
is the minimum version that satisfies the requirement.[email protected]
would also satisfy the requirement.[email protected]
or[email protected]
would not.
- The
^
(caret) prefix, which is the default when younpm install
a new dependency, means that the minor version can change.[email protected]
is the minimum version that satisfies the requirement.[email protected]
or[email protected]
would also satisfy the requirement.[email protected]
would not.
You may see other styles of range in the wild, like >=
(which also allows major versions to change), ||
(one or the other) or even *
(anything goes), but the above are by far the most common.
Check the current version
List installed packages
Because packages are generally installed with ranges, you can't assume that the number that appears in the package file is what's currently installed. For example, for this site at the time of writing, the package file requires @tsconfig/[email protected]
or greater, up to but not including @tsconfig/[email protected]
:
$ npm pkg get devDependencies
{
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
"gh-pages": "^6.0.0",
"typescript": "^4.7.4"
}
Which version actually got installed depends on what versions were available via the registry at the time of installing - the latest version that's compatible with the range will be used at point. Using npm ls
with the package-spec
argument, npm will output the specific version (note that there may be multiple different paths through the dependency tree and even multiple versions installed for a given package name):
$ npm ls @tsconfig/docusaurus
[email protected] path/to/wtf-npm
└── @tsconfig/[email protected]
So 1.0.7
, which satisfies ^1.0.5
, is the version that's installed in node_modules/
. You can also see this specific version stored in the lock file (extracted using jq
):
$ jq '.packages | with_entries(select(.key | contains("@tsconfig/docusaurus")))' package-lock.json
{
"node_modules/@tsconfig/docusaurus": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-1.0.7.tgz",
"integrity": "sha512-ffTXxGIP/IRMCjuzHd6M4/HdIrw1bMfC7Bv8hMkTadnePkpe0lG0oDSdbRpSDZb2rQMAgpbWiR10BvxvNYwYrg==",
"dev": true
}
}
Note that sometimes there may be multiple paths through the dependency tree for a given package. For example, again for this site at the time of writing:
$ npm ls array-union
[email protected] path/to/wtf-npm
├─┬ @docusaurus/[email protected]
│ └─┬ @docusaurus/[email protected]
│ └─┬ [email protected]
│ └── [email protected]
└─┬ [email protected]
└─┬ [email protected]
└── [email protected]
array-union
is a transitive dependency (not specified directly in the package file), hence the depth of the tree, and has two different versions installed simultaneously (via different versions of globby
). You can make the package-spec
argument more specific to focus on the versions you care about, e.g.:
$ npm ls array-union@2
[email protected] path/to/wtf-npm
└─┬ @docusaurus/[email protected]
└─┬ @docusaurus/[email protected]
└─┬ [email protected]
└── [email protected]