Package sanity checks

There are a lot of ways in which developing packages for node can be confusing, especially in an environment where not every contributor is an avid npm package maintainer.

Some common mistakes are easily prevented, and avoiding them will save much time and frustration.

These issues can be solved once and for all with some npm / shell magic.

Run the right node

Likelely nothing will work if we end up running the wrong version of node. Whether that is a too new or too old version, it might cause all kinds of unpredictable behavior.

If the project is using newer node features, running npm start might look like this:

(function (exports, require, module, __filename, __dirname) { async function foo() {
                                                                    ^^^^^^^^

SyntaxError: Unexpected token function
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:373:25)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:140:18)
    at node.js:1043:3

Instead of leaving people to guess what went wrong, we can try and detect this, giving a much friendlier error. To make sure we run the right version for a package we have a few options.

npm engines

The first official tool at our disposal is the package.json engines property. It advises consumers to use a spefic version of node with the engines when the package is installed.

{
  "name": "my-package",
  "engines": {
    "node": "^8.11.0",
    "npm": "^6"
  }
}

This will generate a warning if the package is installed with an unsupported version, according to the semver ranges. Here we are a bit conservative and only want node versions greater than 8.11 but less than 9.

npm WARN engine my-package@1.0.0: wanted: {"node":"^8.11.0"} (current: {"node":"8.9.4","npm":"5.6.0"})

This is not quite enough for enforcing a version when working on the package itself, but it’s our only option for telling consumers about potential compatiblity issues.

Install node locally

An option if the repository requires a specific node version for development, or if the repo is not a package to be published. We can install an exact version of node, which will be installed like an npm package, and added to the path for npm run.

$ npm install nodejs@8.11.3 --save-exact --save-dev

That way we run a small test with node -v and end up with a package.json like this:

{
  "name": "my-package",
  "scripts": {
    "start": "node -v"
  },
  "devDependencies": {
    "node": "8.11.3"
  }
}
$ npm start
v8.11.3

Basically any run-script we add will use the node binary from ./node_modules/.bin instead of the one from the system.

Using nvm

We can add an .nvmrc file to the root of the repo to tell developers which version to use. Whenever we see such a file, it should contain the recommended node version, and running nvm use will make sure we’re running the way the author intended.

$ echo "8" > .nvmrc
$ nvm use
Found '[...]/.nvmrc' with version <8>
Now using node v8.11.3 (npm v6.1.0)

If we embrace the .nvmrc as a standard, we can automate the version switching by adding some shell magic, to look for it and run nvm use if it exists.

The official repo has examples of nvm shell integration

Avoid stale modules: check-versions

Granted, this is probably a non-issue on smaller repositories, but with enough developers working on the same code this is bound to happen pretty often: After pulling the latest version, or changing branches to review some changes, something isn’t quite right. The console is red and the messages are completely incomprehensible. Whoops, the package.json has changed, perhaps with some incompatible version of angular, but the server unknowingly churns out a broken bundle.

Avoid wasting hours trying to debug an inconsistent state of the world, or from losing a potential contributor by adding check-dependencies and catching the problem early.

We can run check-dependencies without an explicit dependency with npx. To check the state of the world, for any package.

$ npx check-dependencies
@tradeshift/document-components: installed: 4.14.0, expected: 4.15.0
@tradeshift/v4: installed: 12.8.1, expected: 12.9.0
Invoke npm install to install missing packages

For instance by putting it in prestart, the program will be unable to start when the contents of node_modules doesn’t match package.json.

{
  "name": "my-package",
  "scripts": {
    "prestart": "check-dependencies",
    "start": "node ./index.js"
  },
  "devDependencies": {
    "check-dependencies": "^1.1.0"
  }
}

There’s a slight caveat though. If we start using npm linked packages to fix an issue in a subdependency, the version in package.json still needs to match, or our package will do as we just asked, and refuse to start.

$ npm start

> Apps@2017.0.0 prestart [...]/Apps
> check-dependencies

@tradeshift/v4: installed: 0.0.0-semantically-released, expected: 12.9.0
Invoke npm install to install missing packages
npm ERR! code ELIFECYCLE
npm ERR! errno 1

Finally, the node api of check-dependencies can be used to customize the behavior, and ignore certain types of mismatches, such as symlinked packages.

But by either disabling the prestart script or specifying the version manually while testing, we can test without the check getting in the wahy.

Posted in Programming with : Node.js

comments powered by Disqus