DEV Community

Adam Goth
Adam Goth

Posted on

Understanding package dependencies within a pnpm monorepo

When you're working on a large project with multiple packages, a monorepo structure can help simplify your development workflow. With a monorepo, you can manage multiple packages as a single entity, which can make it easier to share code and manage dependencies. In this post, we'll take a look at how to structure dependencies in a pnpm monorepo, with examples to help illustrate the concepts.

What is pnpm?

Before we dive into how to structure dependencies in a pnpm monorepo, let's first define what pnpm is. pnpm is a package manager for Node.js that optimizes the installation and sharing of packages across multiple projects. Unlike other package managers like npm and Yarn, pnpm uses a single global cache for all packages, which reduces disk usage and speeds up package installation.

What is a monorepo?

A monorepo is a codebase that contains multiple packages or applications. Rather than storing each package or application in its own repository, a monorepo stores all packages or applications in a single repository. This can make it easier to manage dependencies, share code, and coordinate changes across packages.

In a monorepo, packages are often organized into a packages directory, with each package stored in its own subdirectory. Each package typically has its own package.json file, which lists its dependencies, devDependencies, and scripts.

Structuring dependencies in a pnpm monorepo

When you're working with a pnpm monorepo, there are three types of dependencies that you need to be aware of: dependencies, peerDependencies, and devDependencies.

// package.json
{
  "dependencies": {
    ...
  },
  "peerDependencies": {
    ...
  },
  "devDependencies": {
    ...
  }
Enter fullscreen mode Exit fullscreen mode

Let's take a look at each one.

dependencies

dependencies are packages that your package depends on to function properly. When you install a package's dependencies, pnpm will install them in the package's node_modules directory.

If a dependency is used in multiple packages in the monorepo, it should be listed as a regular dependencies in the package.json files of those packages. However, if a dependency is only used by a single package in the monorepo, it should be listed in that package's package.json file and should not be listed in the root directory's package.json file.

For example, let's say we have a monorepo with the following packages:

my-monorepo/
  packages/
    app/
      package.json
    ui-library/
      package.json
Enter fullscreen mode Exit fullscreen mode

If both the app and ui-library packages are using the same version of lodash, it makes sense to list lodash as a regular dependency in the package.json files of both packages:

// app/package.json
{
  "name": "app",
  "version": "1.0.0",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

// ui-library/package.json
{
  "name": "ui-library",
  "version": "1.0.0",
  "dependencies": {
    "lodash": "^4.17.21"
  }
}
Enter fullscreen mode Exit fullscreen mode

On the other hand, if the app package is the only package that uses react-router-dom, we would only list react-router-dom as a dependency in the app package's package.json file:

// app/package.json
{
  "name": "app",
  "version": "1.0.0",
  "dependencies": {
    "react-router-dom": "^5.3.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

peerDependencies

peerDependencies are packages that your package depends on but that will be provided by the consumer of your package. They are typically used when your package provides functionality that depends on a specific version of another package, but you don't want to include that package as a regular dependency because it will be provided by the consumer of your package.

When you specify a package as a peerDependency in your package.json file, you are essentially saying that "this package requires that the consumer of my package provides this package". The consumer of your package must then ensure that the required package is installed and available to your package when it is used.

For example, let's say we have a monorepo with the following packages:

my-monorepo/
  packages/
    app/
      package.json
    ui-library/
      package.json
Enter fullscreen mode Exit fullscreen mode

Let's say the app package depends on react as a regular dependency, but the ui-library package provides a component that depends on a specific version of react. In this case, we would list react as a regular dependency in the app package's package.json file, and as a peerDependency in the ui-library package's package.json file:

// my-monorepo/packages/app/package.json
{
  "dependencies": {
    "react": "^17.0.2"
  }
}

// my-monorepo/packages/ui-library/package.json
{
  "peerDependencies": {
    "react": "^17.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

This ensures that react is installed in the app package's node_modules directory as a regular dependency, and is also listed as a required peerDependency for the ui-library package. When a consumer installs the ui-library package, they will need to ensure that they have installed a compatible version of react in their own project, which will be used by the ui-library package's component that requires it.

devDependencies

devDependencies are packages that are only used during development and testing, and are not required for the package to run in production. They are typically used for tools like testing frameworks, build tools, and code quality tools.

When you list a package as a devDependency in your package.json file, pnpm will install it in the package's node_modules directory, but it will not be included in the production build of the package.

For example, if you are using Jest as your testing framework, you would list it as a devDependency in your package.json file:

{
  "name": "my-package",
  "version": "1.0.0",
  "devDependencies": {
    "jest": "^27.2.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

In a pnpm monorepo, if a devDependency is used in multiple packages in the monorepo, it should be listed as a regular devDependencies in the package.json files of those packages. However, if a devDependency is only used by a single package in the monorepo, it should be listed in that package's package.json file and should not be listed in the root directory's package.json file.

Conclusion

When you're working with a pnpm monorepo, it's important to understand how to structure your dependencies to ensure that your packages can share code and manage dependencies effectively. By using dependencies, peerDependencies, and devDependencies appropriately, you can create a clean and efficient monorepo structure that is easy to manage and maintain.

Note: The logic regarding dependency structure described in this post applies to most monorepos in general, not just pnpm, however, different package managers may have different ways of managing dependencies and version ranges, so make sure to check the documentation.

Top comments (1)

Collapse
 
lucasdale99 profile image
Lucas Dale

The node_modules/.pnpm still lists the dev dependencies and peer dependencies, and bloats the artifact when you try to create a production build. Currently dealing with this issue with using pnpm for production builds.

If working in Next.js, you can Analyze the production build by adding a wrapper to the next.config.js and running ANALYZE=true pnpm build to see which files are taking up the most space.

Here are some helpful references.
See: nextjs.org/docs/app/building-your-...
See: github.com/vercel/next.js/discussi...