Search code examples
node.jsnpmnpm-package

Sharing Dev Dependencies for Multiple Company Repos via a Private Dependency


We use an entire TypeScript-driven stack at my company (back-end, front-end web, mobile). While creating our ecosystem, we have created a number of private Node packages in order to share as much code as possible. We also use a number of similar dev dependencies across all projects, namely:

One of the main struggles is keeping the versions for these dev dependencies in sync across all projects in order to keep the coding experience consistent across all repos. In the past we tried 2 ways:

  • When deciding to update a tool, open a PR on every single repository to target the same version (with over 15 repos to update a tool in, this became unnecessarily time consuming)
  • Setting up GitHub's Dependabot to update the tools for these, but this ended up being inconsistent due to the default max number of PRs Dependabot can open at once (5), resulting in some repos getting an update on some weeks whether others would wait for another 1-2 weeks depending on the other dependencies that needed to be updated. (This also meant that Dependabot would sometimes prefer opening PRs for these tools over some more "useful" dependencies).

I was hoping to create one shared private package with all of the necessary tools and their respective dependencies (Let's call it the Tools package). The idea was to put all of the needed tools in the dependencies section of the Tools package, and then import only the Tools package in the devDependencies section of all the other repos (for example repo A). Then, whenever I needed to update a tool, I could do all the updates in my Tools package, publish a new version, and in turn update the Tools package on repo A. That way every Node project depended on the one Tool dependency instead of 5/6.

Unfortunately, I haven't found a way to do this smoothly. So far I have tried adding the tools in the dependencies section of Tools, pinning a specific version of Prettier, ESLint, etc. However, when I add Tools as a devDependency on A, npm seems to simply look for the most hassle-free common version and install that at the root level of A's node_modules folder instead of taking the version that would be most compatible across all packages. For example:

  • I used to have ESLint v8.57.0 A's devDependencies
  • I pinned v9.0.0 in Tools
  • I removed the direct devDependency to ESLint and added the Tools devDependency on A, and ran npm install
  • I expected it to install ESLint v9.0.0 top level on A. However, it kept v8.57.0 top level and installed v9.0.0 nested under the Tools subfolder.
  • Therefore, when I called eslint top level in A, it was pointing to the wrong version.

I could fix this by simply deleting the node_modules folder and the package-lock.json file on A and doing a clean reinstall, but having to delete it every time means that I can no longer use Dependabot PRs as it won't do that for me, and I also will never have an assurance that the right version is installed top level without checking the node_modules directory structure to see what is top level and what is nested.

Any recommendations on how to approach this would be greatly appreciated. While I would prefer to use private packages to be able to version correctly, any sort of solution to a similar problem would be great to hear.


Solution

  • npm-dedupe seems to fit the bill for my use case. Running this after npm install will look for any possible deduplications in order to simplify the modules dependency tree. In my use case, running npm dedupe hoists eslint v9.0.0 to the root node_modules folder and removes it from the Tools subfolder, resulting in what I was expecting. There is no guarantee that the tree will look as I expect it to be (for example if there are conflicting versions of eslint, then it will still nest eslint v9.0.0 in Tools) but because a tool like eslint would never be directly depended on I can live with that risk.

    Note: According to the docs, the same result can be achieved by running npm install --prefer-dedupe, but this was not the case for me. Running this command still kept the older version of eslint top level. It's only after running npm dedupe that the dependency tree was corrected.