Protecting your product with npm 'save-exact'

There’s no question that npm and node have a massive open source ecosystem backing them. Each day brings hundreds of new packages and thousands of updates to existing ones.

With a simple npm install we can grab any package we want.

npm install --save joe-schmoes-library

NPM, by default, will save this to our package.json.

// ...
"dependencies": {
  "joe-schmoes-library": "^3.4.11"
}
// ...

The ^ means that any time we run npm install again, npm will only update or dependency if there is a minor or patch change in the semantic versioning. NPM also provides a ~ substitute if we only want patch modifications.

Semantic versioning is robust. It provides flexibility for the package developer to make features and bug fixes without negatively affecting the consumers with a major (breaking) change. The catch with NPM and semantic versioning is that we have to trust the developers who update the code to do the right thing.

Unfortunately, trusting open source developers can be a problem. Nothing stops a developer from making a breaking change during a bug fix.

Production Is Down, Jim

Our production build at C2FO, like most node projects, involves an npm install command during the process. With the power of semantic versioning, our production build introduced a new dependency patch that contained a hidden, breaking change. The change caused a runtime error that prevented users from performing core actions on our application.

We trusted the developer to do the right thing and they made a mistake. Looking at their change in hindsight, it was a clear error. Of course, that would break the consumer’s code base if published. Why did they do that?

Because they’re human.

Protect Your Code From Humans With ‘save-exact’

Automatically updating semantic versioning has its benefits with security and bug fixes being the primary reasons. For us, the benefits weren’t enough, and we opted to remove all automatic updating of our dependencies through NPM’s ^ and ~ prefixes.

Installing an exact version of an NPM module looks like this.

npm install --save --save-exact angular

# you can replace --save-exact with the shorthand version, -E. npm install --save -E angular

The result of adding --save-exact to the install command is an exact version of the install package(s).

// ...
"dependencies": {
  "angular": "1.5.3"
}
// ...

An optional environment variable also has the same effect.

npm_config_save_exact=true

Both of these solutions solve the problem, but in a way that either force us to do more work or by affecting our entire system. A better solution than the cli flag or environment variable is to save this to the npm config, .npmrc.

NPM’s config file, .npmrc

NPM comes with a built-in, inheritance based configuration system. This system is broken up into four files.

  • per-project config file - /path/to/my/project/.npmrc
  • per-user config file - ~/.npmrc
  • global config file - $PREFIX/etc/npmrc
  • npm builtin config file - /path/to/npm/npmrc

The Downside

--save-exact is a near perfect solution for protecting your code against open source development errors. If you want to go whole-hog on package version control, then you will need to lock down the dependencies of your packages recursively. Take a look at the npm shrinkwrap command for more information on this.

Unfortunately, using --save-exact is also a sure-fire way to miss any patches or backwards-compatible features in your dependencies. If you choose to take the exact version approach, then be diligent about upgrading core packages over time.