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": {
...
}
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
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"
}
}
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"
}
}
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
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"
}
}
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"
}
}
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)
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...