Thursday, June 12, 2014

Checking Node.js Project Dependencies with CoffeeScript

I love Node.js. I love CoffeeScript. I have never found it easier to translate concepts in my head to working code, as I do when I'm working with these two. Like this morning, when I was spinning up a new project...

Node.js Background

In Node.js, a project is defined by a package.json file. Here is an abbreviated example from my project telnet-stream:

{
    "name": "telnet-stream",
    "version": "0.0.4",
    "description": "Transform streams for TELNET protocol",
    "devDependencies": {
        "coffee-script": "1.7.x",
        "mocha": "1.19.x",
        "should": "3.3.x"
    }
}


Note the section "devDependencies". This section, along with a section called "dependencies" define the dependencies for a Node.js project. The Node Package Manager (npm) will fetch and recursively install all of your project's dependencies with the following command:

npm install

I've discovered two interesting points about the dependency management here.

One, the semantic versioning is very useful. Note above how I've specified "coffee-script": "1.7.x". When npm installs the coffee-script module, it will automatically grab the latest in the 1.7 series. And that is just scratching the surface of what you can specify with semantic versioning.

Two, you can specify Git URLs as dependencies, including your preferred branch/tag. Need to make a small tweak to a dependency? Easy, clone the repository, make your tweak, and specify the URL to your branch. Managing bleeding edge dependencies couldn't be easier.

Even better, as of version 1.1.65, GitHub URLs can be specified as dependencies using an abbreviated "user/project" syntax. So, if you wanted to use my telnet-stream project:

{
    "dependencies": {
        "telnet-stream": "blinkdog/telnet-stream"
    }
}


If you use GitHub, this is a very convenient way to manage dependencies for and between your projects.

CoffeeScript Background

CoffeeScript is a language that transpiles into JavaScript. That is, you write code in CoffeeScript and the compiler will output JavaScript. Said JavaScript is suitable for running in the browser, or on the server in containers like Node.js.

One of the nice little utilities provided by CoffeeScript is cake. cake is a simple build utility, similar to make; instead of a Makefile, you specify a Cakefile. Unlike a Makefile, a Cakefile has no special format. It is just a CoffeeScript source file with a few helper functions to define tasks.

For example, here is my Cakefile for telnet-stream:

{exec} = require 'child_process'

task 'build', 'Build the module', ->
  compile -> test()

task 'clean', 'Remove build cruft', ->
  clean()

task 'compile', 'Compile CoffeeScript to JavaScript', ->
  compile()

task 'rebuild', 'Rebuild the module', ->
  clean -> compile -> test()
 
task 'test', 'Test with Mocha specs', ->
  test()

clean = (callback) ->
  exec 'rm -fR lib/*', (err, stdout, stderr) ->
    throw err if err
    callback?()

compile = (callback) ->
  exec 'node_modules/coffee-script/bin/coffee -o lib/ -c src/coffee', (err, stdout, stderr) ->
    throw err if err
    callback?()

test = (callback) ->
  exec 'node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register --recursive', (err, stdout, stderr) ->
    console.log stdout + stderr
    callback?() if stderr.indexOf("AssertionError") < 0


I've defined a lot of tasks at the top (build, clean, compile, rebuild, test), but none of those are required. The Cakefile is plain old CoffeeScript, so everything is optional. Note that the five tasks delegate to only three functions that call exec.

Problem

So this morning, I was specifying the dependencies for my project in package.json. This is a project that I intend to deploy for public consumption on the Internet, so stability and managed configuration are important. One of the things that caught my eye was the semantic versioning:

{
    "devDependencies": {
        "coffee-script": "1.7.x",
        "mocha": "1.19.x",
        "should": "3.3.x"
    }
}


If I were to execute npm install today, npm would choose CoffeeScript 1.7.1 to satisfy the "coffee-script": "1.7.x" dependency. However, if I were to install next month (or next year), there may be a new version of CoffeeScript in the npm repository. Installing at that time might give me CoffeeScript 1.7.8.

The convention is that minor point changes shouldn't affect the public API. That is, the difference between 1.7.1 and 1.7.8 might be internal bug fixes or optimizations. We do not expect that major components in the module will be removed, renamed, or exhibit wildly different behavior.

Because this is intended to be a production-grade service, a managed configuration is important. Or: It is not wise to bet the behavior of the service on how well a dozen+ authors of third-party modules can follow the versioning conventions. Despite the power of npm's semantic versioning, it is unwise to trust it with the final configuration of a public facing production-grade service.

For the project, this means the dependencies have to be specified precisely. If we say 0.1.6, we mean, version 0.1.6 and no other. This solves the managed configuration problem, but it does open some other problems:

  • How can we be sure that we're not missing bug fixes, especially security related bugs?
  • How can we be sure our codebase evolving with its components, and staying on the cutting edge of their features?

Just because our configuration is managed doesn't mean that it can't change or evolve. The key here is that configuration changes shouldn't be a surprise. It is okay to use version 0.1.7 instead of version 0.1.6, if you've reviewed the changes and know what differences to expect in production.

We just need a tool to tell us which dependencies have been updated and require review...

Solution

I thought that it would be neat if I could look up my project's dependencies and determine if I've specified the latest version or not. So I wrote a Cakefile task to do that:

{exec} = require 'child_process'

task 'depcheck', 'Check dependency versions', ->
  project = require './package.json'
  for dependency of project.dependencies
    checkVersion dependency, project.dependencies[dependency]
  for dependency of project.devDependencies
    checkVersion dependency, project.devDependencies[dependency]


checkVersion = (dependency, version) ->
  exec "npm --json info #{dependency}", (err, stdout, stderr) ->
    depInfo = JSON.parse stdout
    if depInfo['dist-tags'].latest isnt version
      console.log "[OLD] #{dependency} is out of date #{version} vs. #{depInfo['dist-tags'].latest}"



And here is the output of a contrived example:

tux@laptop ~/NetBeansProjects/sekrit-project $ cake depcheck
[OLD] should is out of date 3.3.0 vs. 4.0.4
[OLD] sansa is out of date 0.1.4 vs. 0.1.6
[OLD] express is out of date 4.4.1 vs. 4.4.3
[OLD] body-parser is out of date 1.2.1 vs. 1.3.1
[OLD] supertest is out of date 0.11.2 vs. 0.13.0
[OLD] mocha is out of date 1.18.0 vs. 1.20.1
[OLD] coffee-script is out of date 1.7.0 vs. 1.7.1


A dozen lines of CoffeeScript and now a single command tells me if any of my project's dependencies are out of date. Did I mention that I love Node.js and I love CoffeeScript?

Thoughts

What if I wanted to do something like this with Java and Maven?