Don't Use Semver Ranges to Specify Package Dependencies
Semver is a versioning format of the form major.minor.patch that emerged for use with the package manager NPM, primarily used in the Node.js ecosystem. Package details are defined by a package.json file, specifying such things as the version and the versions of all of the packages that are immediate dependencies.
{ "name": "example-package", "version": "0.1.0", "dependencies": { "another-package": "1.0.0" } }
Semver incorporates many ways to describe version ranges. The more commonly used methods involve wildcards and prefixes: 1.2.* is the same as ~1.2.0, meaning any patch release such as 1.2.3 is acceptable, while 1.*.* or 1.* is the same as ^1.2.3, meaning any later minor release such as 1.4.9 is acceptable.
{ "name": "example-package", "version": "0.1.0", "dependencies": { "another-package": "~1.0.0", "a-third-package": "0.*.*" } }
It is sadly commonplace to see these range designations used in package.json files throughout the Node.js ecosystem, as to my eyes this is something that should never be done.
Semver Ranges are a Source of Random Breakage
Consider the standard approaches to build and deployment pipelines that use NPM: at some point packages will be installed at the latest versions allowed by the semver ranges specified. When a new version is released, that version will be used the next time a build or deployment occurs. Do you trust all of the disparate authors of your dependent packages to accurately update major, minor, or patch versions with respect to whether or not they are changing APIs or other aspects of their packages that you depend on? Do the packages even have rigorously defined APIs? Do you trust package maintainers to always produce bug-free releases? Do you have 100% automated test coverage of every feature and interaction in your application that might fall prey to subtle interaction issues due to a new dependency release? Specifying a range of yet-to-be-released versions rather than a specific known version introduces a source of random, uncontrolled change and breakage to a build and deployment system. There is no good reason for it.
Since package.json files only specify immediate dependencies, and many core and frequently used packages use semver ranges for their immediate dependencies, any NPM-based build and deployment process for a significant application is at the mercy of potentially hundreds of ongoing updates in any given year. Is it really the case that we should expect that none of these will break critical functionality in potentially very subtle ways? Of course not. This is why semver ranges should never be used in package.json dependencies. You should stop doing it, and you should encourage everyone else to stop doing it.
Updating a dependency version is the same as updating code. It should go through a QA and release process as an organized, planned, known change. The state of the application should be as insulated as possible from outside, unplanned changes in order to minimize additional unnecessary work and defects. Given all of this, it seems to me somewhat crazy that the Node.js ecosystem makes such prolific use of semver ranges to specify dependencies. This serves no useful purpose, and only makes it harder to rigorously develop and release applications.
Meanwhile, npm shrinkwrap
This is all of course a known problem. There is even a way to deal with it baked into NPM, fixing specific sub-dependency versions via the shrinkwrap command and dependencies format:
{ "name": "example-package", "version": "0.1.0", "dependencies": { "another-package": { "version": "1.0.0", "dependencies": { "a-third-package": "0.1.2" } } } }
This still all seems like a big circular carnival of needless work and risk that would go away if people just all agreed to specify distinct versions rather than version ranges for their dependencies.