Skip to content

Instantly share code, notes, and snippets.

@pradeepsng30
Created April 6, 2019 09:32
Show Gist options
  • Save pradeepsng30/4ef6be7115186a4aa591a66c1321c7d3 to your computer and use it in GitHub Desktop.
Save pradeepsng30/4ef6be7115186a4aa591a66c1321c7d3 to your computer and use it in GitHub Desktop.
npm dependencies
example, the resulting directory structure would look something like this:
node_modules/
├── foo/
│ └── node_modules/
│ ├── hello/
│ └── world/
└── bar/
└── node_modules/
├── hello/
└── goodbye/
Notably, the directory structure very closely mirrors the actual dependency tree. The above diagram is something of a simplification: in practice, each transitive dependency would have its own node_modules directory and so on, but the directory structure can get pretty messy pretty quickly. (Furthermore, npm 3 performs some optimizations to attempt to share dependencies when it can, but those are ultimately unnecessary to actually understanding the model.)
To make this a little more concrete, let’s look at a couple of examples. First off, let’s take a look at some simple cases, starting with some uses of ramda:
import { merge, add } from 'ramda'
export const withDefaultConfig = (config) =>
merge({ path: '.' }, config)
export const add5 = add(5)
The first example here is pretty obvious: in withDefaultConfig, merge is used purely as an implementation detail, so it’s safe, and it’s not part of the module’s interface. In add5, the example is a little trickier: the result of add(5) is a partially-applied function created by Ramda, so technically, a Ramda-created value is a part of this module’s interface. However, the contract add5 has with the outside world is simply that it is a JavaScript function that adds five to its argument, and it doesn’t depend on any Ramda-specific functionality, so ramda can safely be a non-peer dependency.
Now let’s look at another example using the jpeg image library:
import { Jpeg } from 'jpeg'
export const createSquareBuffer = (size, cb) =>
createSquareJpeg(size).encode(cb)
export const createSquareJpeg = (size) =>
new Jpeg(Buffer.alloc(size * size, 0), size, size)
In this case, the createSquareBuffer function invokes a callback with an ordinary Node.js Buffer object, so the jpeg library is an implementation detail. If that were the only function exposed by this module, jpeg could safely be a non-peer dependency. However, the createSquareJpeg function violates that rule: it returns a Jpeg object, which is an opaque value with a structure defined exclusively by the jpeg library. Therefore, a package with the above module must list jpeg as a peer dependency.
This sort of restriction works in reverse, too. For example, consider the following module:
import { writeFile } from 'fs'
export const writeJpeg = (filename, jpeg, cb) =>
jpeg.encode((image) => fs.writeFile(filename, image, cb))
The above module does not even import the jpeg package, yet it implicitly depends on the encode method of the Jpeg interface. Therefore, despite not even explicitly using it anywhere in the code, a package containing the above module should include jpeg as a peer dependency.
They key is to carefully consider what contract your modules have with their dependents. If those contracts involve other packages in any way, they should be peer dependencies. If they don’t, they should be ordinary dependencies.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment