Workspace Dependency Resolution and Version Management
Dependency management in a monorepo differs from single-package projects. When your component library and multiple apps all depend on React, you want one copy of React shared, not five separate copies bloating your node_modules. When your utilities package needs a specific version of lodash but your components need another, you must resolve these conflicts without breaking either. This article covers the workspace:* protocol, hoisting dependencies, handling peer dependencies, and resolving version conflicts in a monorepo using pnpm.
Understanding Monorepo Dependency Types
In a monorepo, dependencies fall into three categories:
- Workspace dependencies: packages in your own monorepo, linked via
workspace:*. - Peer dependencies: frameworks or major libraries your package requires but does not own (React, Vue, lodash).
- Development dependencies: build tools and test utilities used only during development.
Each type is managed differently to avoid duplication and version conflicts.
Using the workspace:* Protocol
The workspace:* protocol tells pnpm to use the local workspace version of a package instead of fetching from npm. This is the foundation of internal monorepo linking.
In your app's package.json:
{
"dependencies": {
"@myapp/ui-library": "workspace:*",
"@myapp/design-tokens": "workspace:*"
}
}
When you run pnpm install, pnpm creates symlinks to these packages in your node_modules. If you update a component and run the dev server, the change is immediately reflected without rebuilding. The workspace:* means "use any version in the workspace"; you can also pin to a specific version:
{
"dependencies": {
"@myapp/ui-library": "workspace:^1.0.0"
}
}
At publish time, pnpm converts workspace:* to a real version number (e.g., ^1.0.0), so external packages install the correct version from npm.
Managing Peer Dependencies
Peer dependencies declare that your package requires a framework but does not bundle it. In your UI library package.json:
{
"name": "@myapp/ui-library",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": false
}
}
}
This tells npm: "If you install @myapp/ui-library, you must also install React 18+." Apps that use your library are responsible for providing React. Internally, in your monorepo, you still need React as a devDependency for building:
{
"devDependencies": {
"react": "workspace:*",
"react-dom": "workspace:*"
}
}
This allows the library to build and test locally without forcing consumers to install React multiple times.
Resolving Peer Dependency Conflicts
If app A requires React 18 and app B requires React 16, pnpm can install both if you configure resolutions. In the root package.json:
{
"pnpm": {
"overrides": {
"react": "^18.0.0"
}
}
}
This forces all packages in the monorepo to use React 18, preventing conflicts. Use overrides (formerly resolutions in yarn) carefully: only for critical shared libraries. If app B truly needs React 16, consider upgrading or maintaining it as a separate monorepo.
Hoisting Common Dependencies
pnpm uses a "hoisting" strategy to place frequently used dependencies at the root node_modules for efficiency. Most dependencies are hoisted automatically. To explicitly hoist a dependency to the root (so all packages share one copy), use the root pnpm-workspace.yaml:
packages:
- 'apps/*'
- 'packages/*'
shared-workspace-lockfile: true
node-linker: hoisted
The node-linker: hoisted ensures all shared dependencies are installed at the root, visible to all packages. This reduces disk usage and build time.
Dependency Version Ranges and Caret Ranges
When specifying dependencies, version ranges matter:
"react": "18.0.0"— exact version, never updates."react": "^18.0.0"— caret range, allows patches and minor updates (18.0.0, 18.2.5, etc.) but not 19.0.0."react": "~18.0.0"— tilde range, allows only patch updates (18.0.0, 18.0.5) but not 18.1.0.
For peer dependencies in libraries, use caret ranges (^18.0.0) to allow flexibility for consumers. For internal workspace dependencies, use workspace:* to always use the latest local version.
Checking Dependency Graphs
Turborepo automatically builds a dependency graph. View it with:
turbo build --graph
This generates a graph.html file showing which packages depend on which. Use this to identify circular dependencies or unexpected coupling. Your graph should show a DAG (directed acyclic graph), not circles.
To list all dependencies of a single package:
cd packages/ui-library
pnpm ls
This shows the resolved dependency tree for that package.
Updating Dependencies in Monorepos
Update all dependencies at the root:
pnpm update
Update a specific package:
pnpm update react@latest
Or update at a specific workspace:
cd packages/ui-library
pnpm update typescript@latest
After updating, regenerate the lockfile and test all packages:
turbo test
Handling Workspace Dependency Upgrades
When you upgrade a dependency in your monorepo, propagate the change carefully:
- Update the dependency in the root or specific packages.
- Run
pnpm installto updatepnpm-lock.yaml. - Build all packages to catch breaking changes early:
turbo build --filter=...
- Run tests across the monorepo:
turbo test
- Commit the lockfile changes.
If a dependency breaks one app but not others, mark it as a dev-only dependency in that app, or use pnpm.overrides to enforce the version all apps must use.
Key Takeaways
- Use
workspace:*to link internal packages in development and let pnpm convert to real versions at publish time. - Declare peer dependencies for frameworks (React, Vue); define devDependencies for local testing.
- Use
pnpm.overridesin the rootpackage.jsonto resolve conflicts by enforcing a single version across all packages. - Check your dependency graph with
turbo build --graphto ensure no circular dependencies exist. - Always run tests after updating dependencies to catch breaking changes early.
Frequently Asked Questions
What is the difference between peerDependencies and dependencies?
Dependencies are bundled with your package and are managed by npm. Peer dependencies are external requirements you do not bundle; the consuming app must provide them. Use peer dependencies for React, Vue, and other frameworks.
How do I find circular dependencies in my monorepo?
Run turbo build --graph and open the HTML file. Look for arrows that form loops. Restructure your packages so they follow a DAG: utilities < components < apps, with no reverse dependencies.
Can I have different versions of React in different apps?
Technically yes with pnpm.overrides, but it is not recommended. Keep major versions aligned across your monorepo to simplify maintenance. If an app truly needs a different version, isolate it or consider a separate monorepo.
How do I use workspace:* with version constraints?
You can specify a range: "@myapp/ui-library": "workspace:^1.0.0". pnpm ensures the workspace has a version matching the range, then converts it to a real version at publish time.
Should I commit pnpm-lock.yaml to git?
Yes. The lockfile ensures everyone and every CI run uses the same dependency versions, preventing "works on my machine" issues. Commit it as a best practice.