Danger + Dependencies

#Before we get started

This guide continues after “Getting Started” - so you should have seen Danger comment on your PRs.

#Keeping on top of your dependencies

Building pretty-much anything in the node ecosystem involves using using external dependencies. In an ideal situation you want to use as few dependencies as possible, and get the most use out of them. Remember that you are shipping your dependencies too, so you are responsible for them to your end-users.

The numerical scale of dependencies can make it tough to feel like you own your entire stack. So let’s try and use Danger to give us more insight into changes related to our dependencies.

#Lockfiles

The simplest rule, which we can evolve, is that any time your package.json changes you probably want a change to the yarn.lock or shrinkwrap.json file. Yes, not every change to the package.json represents a dependency update but we’re starting simple. You start off your Dangerfile like this:

import { danger, fail, warn } from "danger"
import includes from "lodash.includes"

const hasPackageChanges = includes(danger.git.modified_files, "package.json")
const hasLockfileChanges = includes(danger.git.modified_files, "yarn.lock")
if (hasPackageChanges && !hasLockfileChanges) {
  warn("There are package.json changes with no corresponding lockfile changes")
}

This uses lodash’s _.includes() function to see if danger.git.modified_files includes the package, but not the lockfile.

#Vetting New Dependencies

This works, and for a while, this is enough. Time passes and you hear about a node module with a CVE against it, let’s call it "spaced-between", you want to ensure it isn’t added as a dependency.

There are two aspects that you consider:

#Keeping track of changes to dependencies

We can use danger.git.JSONDiffForFile to understand the changes to a JSON file during code review. Note: it returns a promise, so we’ll need to use schedule to make sure it runs async code correctly.

const blacklist = "spaced-between"

schedule(async () => {
  const packageDiff = await danger.git.JSONDiffForFile("package.json")

  if (packageDiff.dependencies) {
      const newDependencies = packageDiff.dependencies.added
      if (includes(newDependencies, blacklist)) {
        fail(`Do not add ${blacklist} to our dependencies, see CVE #23")
      }
  }
})

So for example with a diff of package.json where spaced-between is added:

{
  "dependencies": {
    "commander": "^2.9.0",
    "debug": "^2.6.0"
+    "spaced-between": "^1.1.1",
    "typescript": "^2.2.1",
  },
}

JSONDiffForFile will return an object shaped like this:

{
  dependencies {
    added: ["chalk"],
    removed: [],
    after: { commander: "^2.9.0", debug: "^2.6.0",  spaced-between: "^1.1.1",  typescript: "^2.2.1" },
    before: { commander: "^2.9.0", debug: "^2.6.0", typescript: "^2.2.1" },
  }
}

Danger can then look inside the added keys for your blacklisted module, and fail the build if it is included.

#Parsing the lockfile

You can trust that this dependency is going to be added directly to your project without it being highlighted in code review, but you can’t be sure that any updates to your dependency tree won’t bring it in transitively. A transitive dependency is one that comes in as a dependency of a dependency, one which isn’t added to packages.json but is in node_modules. So you’re going to look at a simple rule that parses the text of the file for your blacklisted module.

import fs from "fs"
import contains from "lodash-contains"

const blacklist = "spaced-between"
const lockfile = fs.readFileSync("yarn.lock").toString()

if (contains(lockfile, blacklist)) {
  const message = `${blacklist} was added to our dependencies, see CVE #23`
  const hint = `To find out what introduced it, use \`yarn why ${blacklist}\`.` 
  fail(`${message}<br/>${hint}`)
}

Note the use of readFileSync, as Danger is running as a script you’ll find it simpler to use the synchronous methods when possible. You could improve the above rule by making danger run yarn why spaced-between and outputting the text into the messages. We do this in the danger repo with child-process and execSync.

#Building from here

This should give you an idea on how to understand changes to your node_modules, from here you can create any rules you want using a mix of JSONDiffForFile, fs.readFileSync and child_process.execSync. Here are a few ideas to get you started:


Got improvements? Help improve this document via sending PRs.