In this post, I want to answer the following question for Rubyists:

What do the tilde ~ and caret ^ designations mean for version constraints in a package.json file?

To answer this question, we'll compare how Rubyists declare Ruby project dependencies in a Gemfile with conventions used to declare NPM module dependencies in a package.json file.

Of note, some projects use both Gemfile and package.json. For example, a newly created Rails 6 application will have generated a package.json file because, by default, it ships with webpack and related NPM dependencies to compile JavaScript assets.

It might include a section like this:

"dependencies": {
  "@rails/ujs": "^6.0.0",
  "@rails/webpacker": "~4.2.1",

If you're a Rubyist and the version syntax looks odd, then this post is for you.

Subscribe to my newsletter, Joy of Rails, to get notified about new content.

Version constraints in Gemfile

Like the Gemfile, package.json has a convention to specify version constraints. Both Ruby and NPM dependencies usually follow SemVer, that will format a constraint as major.minor.patch, i.e. the declaration "webpack": "4.41.2" indicates webpack major version 4, minor version 41, and patch version 2.

Where they differ is in the use of special characters to declare acceptable ranges. Let's refresh the conventions used in the Gemfile.

To lock a gem dependency to an exact version, we would declare the gem's name and its version as follows:

gem "devise", "4.7.1"

A more optimistic constraint would be to provide an open-ended range that will install or update to a version of the gem that satisfies the range.

gem "devise", ">= 4.7"

To limit the upper end of the range, say, to allow minor updates up to the next major version:

gem "devise", ">= 4.7", "< 5"

This format has a shorthand notation, the squiggly arrow ~>, or the pessimistic version constraint.

gem "devise", "~> 4.7"

The upper end of the range is determined by the smallest level of the declared constraint. For example,

To specify "no constraint", simply omit the version argument.

gem "devise"

For more info, check out the guide on RubyGems.

Version constraints in package.json

NPM conventions provide similar flexibility with alternate syntax.

Let's consider a package.json file that declares @rails/webpacker as a dependency, the following would enforce an exact version:

"@rails/webpacker": "4.2.1",

As with the Gemfile, comparison operators can be used as in the following examples:

NPM supports alternate syntaxes for specifying ranges, including, but not limited to, caret ^ and tilde ~.

Tilde ranges

NPM ~ is like Gemfile ~>

Tilde ranges for NPM are equivalent to Ruby's pessimistic version constraint, the squiggly arrow ~>. In other words, the upper end of the range is determined by the smallest level of the declared constraint:

Caret ranges

NPM ^ is like Gemfile ~> x.0 for versions 1 and up and ~> 0.x.0 for versions less than 1 and greater than 0.0.1

Caret ranges are another take on pessimistic version constraints that do not have a shorthand equivalent in Ruby, i.e., to my knowledge, they're a special breed. They allow patch and minor updates for versions >1.0.0, patch updates for versions <1.0.0 >=0.1.0, and no updates for versions <0.1.0 (except preleases, e.g. 0.0.3-beta). My understanding is that the caret is the answer for traditional SemVer, i.e., there will be breaking changes prior to 0.1.0, there may be breaking changes between minor versions prior to 1.0.0, and there may only be breaking changes between major versions above 1.0.0. Examples:

Bonus syntax in package.json

NPM also supports hyphen ranges and x-ranges, neither of which have Gemfile equivalents as well.

Hyphen ranges

NPM hyphen-ranges are like separate comparison operators in a Gemfile

For hyphen ranges, range inclusivity is tied to specificity of the declared versions:


NPM x-ranges behave like Gemfile ~> with exceptions

X-ranges are mostly self-explanatory as the x denotes any value:

A partial version range is treated as an x-range:


For Rubyists out there who needed an introduction to NPM version constraints, I hope this was a helpful guide, or perhaps a future cheatsheet.

Mostly I wrote this for myself because I tend to forget 😅.

Discuss it on Twitter · Published on Jan 29, 2020

More posts

jQuery plugins in webpack without jQuery

Upgrading jQuery plugins to work with webpack is a common source of confusion. If you're lucky, you may find they can work in either context such that you might not need jQuery at all.

25 reasons to switch to Webpack(er)

There are plenty of great reasons to switch to Webpacker, including improvements in supported syntax, development tooling, performance optimizations, and more. For Rails developers considering the upgrade from the Rails asset pipeline, start here.

3 ways webpack surprises web developers

When I first started working with webpack, I was in for a few surprises. I assumed how things should behave, based on my previous experience with the Rails asset pipeline, only to learn through experience how I was wrong.

Photo by 拴 张 on Unsplash