Override Nested Dependencies With NPM 3

N

pm is one of the primary reasons that the node community is so strong today. It makes it easy to write, package and publish code. This is primarily because of how it solves the package version and dependency crisis - Every package has a version and it's own set of dependencies which are organized into a directory tree. It sounds so simple, but it took over twenty years of developers pulling their hair out over package manager dependency soup, it is a wonder why it hadn't been done sooner. Even more so, NPM's package manifest is a simple json file that lets fine tune the specificity of the modules in your package

However, there is one thing that can still creep up an bite you once in a while. There is no way to manage nested version, or dependencies of dependencies. For example, lets say we have a project - mypackage with two dependencies - modulea and moduled. Just like you would expect, both modules will bring with it whatever packages they might need to function.

.
`-- mypackage
    `-- node_modules
        |-- modulea
        |   |-- @1.8.4
        |   `-- moduleb
        |       |-- @1.1.2
        |       `-- modulec
        |           `-- @0.2.4
        `-- moduled
            |-- @3.4.1
            `-- modulex
                `-- @1.9.3

All fine and good, but lets say thatmoduleb a has elements that are compiled against node and in your efforts to upgrade the the latest LTS version the module fails to compile and the install fails. Luckily, the author of moduleb has published an update that fixes the problem. However, the maintainer of modulea hasn't touched the code in almost a year and there are no updates in sight. And in this case, that is OK. modulea works just fine, it is moduleb that we need change. In any event, we are in a bit of a pickle. The package.json file for npm doesn't provide a way to define nested dependencies, as that is left up to package authors.

We don't have time to open a pull request, hope the respond, make a fix and publish a package. More over, we don't really want to maintain a fork and publish a divergent package. What we really want to do is override the dependency on moduleb defined by modulea. moduled is fine and we don't want to mess with that. We want our dependency tree to look something like this:

.
`-- mypackage
    `-- node_modules
        |-- modulea
        |   |-- @1.8.4
        |   `-- moduleb
        |       |-- @1.4.0
        |       `-- modulec
        |           `-- @0.5.0
        `-- moduled
            |-- @3.4.1
            `-- modulex
                `-- @1.9.3

Luckily, NPM does have shrinkwrap which creates a manifest file that defines the version of every module in the node_modules source tree, including nested dependencies, and skips the semver package resolution. In my experience, for larger projects, using any kind of a lock file for the entire project just leads to unwanted headaches.

Shrink Wrap [shringk']-rap -n, --noun.

  1. locks down the versions of a package's dependencies so that you can control exactly which versions of each dependency will be used
  1. to wrap and seal in a flexible film that, when exposed to a heat, shrinks to the contour of the merchandise.

Enter NPM 3. NPM 3 added the ability to only specify fragments of the dependency tree and falls back to the default semver resolution for the packages that are not specified. Not the ideal situation, but we can make this work. We just need to make a shrink wrap file to lock the versions we want

// npm-shrinkwrap.json
{
  "name":"mypackage",
  "version":"1.0.0"
  "dependencies": {
    "modulea": {
      "version": "1.8.0",
      "from": "modulea@1.8.0",
      "resolved": "https://registry.npmjs.org/modulea/-/modulea-1.8.0.tgz",
      "dependencies": {
        "moduleb": {
          "version": "1.4.0",
           "from": "moduleb@1.4.0",
            "resolved": "https://registry.npmjs.org/moduleb/-/moduleb-1.4.0.tgz"
            "modulec":{
              "version":"0.5.0",
              "from":"modulec@0.5.0",
              "resolved":"https:registry.npmjs.org/modulec/-/modulec.0.5.0.tgz"
            }
          }
      }
  }
}

Now, when you npm install you will get modulea@1.8.0, moduleb@1.4.0, and modulec@0.5.0. We are able to fix the immediate problem with out having to do any major re-writes, we don't have to wait for pull requests and new packages to be published. And if and when all of those things happen, all we'll need to do is delete the shrinkwrap.json file and things are back to normal.

modules npm packages node.js