Search code examples
node.jsnpmpeer-dependencies

npm: refer to a peer dependency; how to align the version from a peer dependency


In abstract, I'm ok with the provided version of dependency-B, which is already installed thanks to dependency-A.

  "dependencies": {
    "dependency-A": "x.y.z",
   }
$> npm ls --depth=1
├─┬ [email protected]
│ ├── [email protected]

So when I require('dependency-B'), I'll expect A's dependency.
I'm using the root function from that library and, in fact, if dependency-A bumps the version, I'd like to align with it and use the same version it uses.

If dependency-B is listed on the dependencies, a brand new package will be installed.

  "dependencies": {
    "dependency-A": "x.y.z",
    "dependency-B": "a.b.c",
   }
$> npm ls --depth=1
├─┬ [email protected]
│ ├── [email protected]
│ ├── ...
├─┬ [email protected]

I'm tempted to not list dependency-B on my dependencies. Should I avoid this practise? Isn't ok to rely on the peer version installed by my main dependency?


If this is a brad practise, how can I tell npm to give me the very same version it's installed by another package?

  "dependencies": {
    "dependency-A": "x.y.z",
    "dependency-B": "~try the one that is installing dependency-A~",
   }

Solution

  • tl;dr: You should always have all dependencies that you're using in your own dependencies object, as conformant implementations of package managers are not required to give you access to your dependencies' dependencies.

    This is an interesting question, and I can think of two scenarios in which you might encounter this:

    1. Both your package and dependency-A use dependency-B independently, for your own set of reasons, and you simply don't care which version to use.
    2. You need to use dependency-B in order to interact with dependency-A, by creating objects of B or receiving objects of B created by A.

    Scenario 1: Independent usage

    If you and your dependency need the same package but don't need to share anything about it, Node gives you the amazing ability of using different versions of the same package in different places by specifying different versions in the package.json of your package and your library's. This is one of the strengths of the Node module system.

    Your situation, however, is that you don't care about the actual version of the package (which makes me think this is not your scenario). In particular, you wonder if it's just better to not define anything in your own package.version and just let Node find your dependecy's dependency.

    This last situation is only possible because you're using npm, and npm does one particular thing: it flattens the module tree in an effort to deduplicate packages, that is, so that multiple dependency specifications that can be satisfied by the same version are, in the end, using the exact same version. This reduces both the size and depth of the module tree, but creates the unintended consequence that you now have access to packages you havent specified as dependencies, just because they were installed in you node_modules directory for the purpose of deduplication.

    This is not the only possible strategy though, and pnpm, another package manager, instead useds symlinks to achieve the same goals. I won't enter into much detail, but pnpm installs all dependencies in a different, system-wide (or user-specific) directory, and then symlinks from your node_modules (and from the dependencies' own node_modules) to the appropriate location in that folder. This achieves not only project-wise deduplication, but system-wide deduplication, as all of your projects using a specific package version will use the same installation. The consequence of this system, though, is that you "lose" the ability to use your dependencies' dependencies in your own package, because they're no longer physically in node_modules.

    Apart from all that, is the idea that you don't care about the version they use. That's almost never the case, as the whole point of semantic versioning is to avoid or contain breakage due to dependency version upgrades. You don't care about the version you use now, but if that package gets upgraded in your dependency to a different major version, your package can break unexpectedly.

    In conclusion, not defining a dependency that you are going to use anyway is a bad practice, both because it prevents other developers from using your package in a different package manager, and because it opens you to unexpected breakage that you won't be able to properly manage.

    Scenario 2: Dependent usage

    The more likely scenario given your description of the problem is that at some point in your usage of dependency-A, either it asks for something or returns something from dependency-B. In this situation it is desirable that both use the same, or at least compatible versions, so that all assumptions about the shape of the objects that are being exchanged hold.

    The correct way of specifying this situation is to explicitly declare dependency-B as a peer dependency of dependency-A. If that's not the case, they're not being correct and you should most definitely bring that up in an issue if possible. As a workaround, you might just declare the same version as them and be wary o possible breakages due to version upgrades on their part. Not defining anything in your own package.json can have the same problems as in Scenario 1.

    However, another possibility is that you don't even need to require that dependency. It might be the case that they expect you to pass data, functions, objects or anything that will be further passed to dependency-b, but in a way that shields you from ever having to interact with dependency-B directly. In this situation, they're essentially incorporating part of B's API into their own, and therefore any breaking change from dependency-B should also incur in a breaking change of dependency-A. This shields you from unexpected breakages, avoids you having to define anything in your package.json and means you're safe.