Sharing dependencies in a JavaScript monorepo

The monorepo structure is an increasingly popular way to organize JavaScript code. In a JavaScript monorepo, you typically have a package.json file at the root of your project that points to the package.json files of the packages in your monorepo. These child package.json files are typically stored at packages/project-name/package.json.

Motivation for moving to a monorepo

At my current job, I'm leading the push to reorganize our frontend into a monorepo structure. We currently have one main React app. I'm working towards moving our marketing pages to a static site generator and another team at my company is building a new design system that will contain our basic React components. Housing these three packages in a monorepo makes sense as they will share code and have many identical dependencies.

Benefits of a monorepo

By placing them in a monorepo, we get some neat benefits. One benefit is that we can importing from one package into another directly without publishing to a package registry. Additionally, Yarn will automatically de-duplicate dependency code stored in our node_modules folders when multiple packages depend on the same version of a dependency.

The gotcha, of course, is that your packages have to have the exact same version dependencies. This can be a tough problem to solve. You can try setting all your packages to have the same dependency strings, but that's easy to break. It just takes one person to upgrade a dependency in one package but not in another to cause problems.

Enforcing identical dependencies in a monorepo

The way around the weakness above is to only allow a single package in your monorepo to import a dependency. Then export that dependency and make the other packages import it from their sibling. Here's what that looks like:

In your project root:

// package.json

{
  "workspaces": [
    "packages/*"
  ],
  ...
}

In your package that imports shared dependencies:

// packages/shared-dependencies/package.json

{
  "dependencies" : {
    "react": "16.x",
    ...
  },
  ...
}
// packages/shared-dependencies/react.js

export * from "react"; // to re-export named exports
export { default } from "react"; // to re-export the default export

In each of your packages that needs the shared dependency:

// packages/web-app/package.json

{
  "dependencies" : {
    "shared-dependencies": "*",
    ...
  },
  ...
}
// packages/web-app/src/App.js

import React, { useState } from "shared-dependencies/react"

Sources

Photo by Elaine Casap