
Pnpm Workspace
- 142 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Set up pnpm workspaces, shared internal packages, filters, and hoisting rules for TypeScript monorepos.
About
Teaches agents to initialize and maintain pnpm monorepos: workspace manifests, inter-package dependencies, filter-based scripts, and hoisting conventions so multi-package SaaS and CLI repos stay fast and consistent for teams.
- Workspace layout
- Shared packages
- pnpm filters
- Hoisting rules
- Internal deps
Pnpm Workspace by the numbers
- 142 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #467 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill pnpm-workspaceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 142 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Set up pnpm workspaces, shared internal packages, filters, and hoisting rules for TypeScript monorepos.
Files
pnpm Workspace
Overview
pnpm workspaces provide built-in monorepo support through pnpm-workspace.yaml, the workspace: protocol for local package linking, and powerful filtering to run commands across specific packages. Catalogs enforce consistent dependency versions across all workspace packages.
When to use: Multi-package repositories, shared libraries with consuming apps, consistent dependency management across packages, running commands on subsets of packages.
When NOT to use: Single-package projects, projects already using npm/yarn workspaces (migration required), projects that need floating dependency versions per package.
Quick Reference
| Pattern | API / Config | Key Points |
|---|---|---|
| Define workspace | pnpm-workspace.yaml with packages globs | Globs match directories containing package.json |
| Link local package | "dep": "workspace:*" | Always resolves to local workspace package |
| Link with version range | "dep": "workspace:^1.0.0" | Fails install if local version does not satisfy range |
| Default catalog | catalog: key in pnpm-workspace.yaml | Single source of truth for dependency versions |
| Named catalog | catalogs: key with named groups | Multiple version sets (e.g., react18, react17) |
| Use catalog in package | "dep": "catalog:" or "dep": "catalog:name" | Resolved to actual version on pnpm publish |
| Filter by name | --filter <name> or -F <name> | Exact name or glob pattern (@scope/*) |
| Filter with dependencies | --filter "foo..." | Package and all its dependencies |
| Filter with dependents | --filter "...foo" | Package and all packages that depend on it |
| Filter by directory | --filter "./packages/app" | All packages under a directory path |
| Filter by git changes | --filter "[origin/main]" | Packages changed since a commit or branch |
| Exclude from filter | --filter "!foo" | Remove matching packages from selection |
| Run script in package | pnpm --filter <pkg> <script> | Runs script only in matched packages |
| Recursive run | pnpm -r run <script> | Runs script in all workspace packages |
| Install all | pnpm install | Single lockfile for entire workspace |
| Publish workspace pkg | pnpm publish | Replaces workspace: and catalog: with real versions |
| Inject workspace deps | inject-workspace-packages=true in .npmrc | Hard-links instead of symlinks; required for deploy |
| Script security (v10+) | allowBuilds in package.json | Lifecycle scripts blocked by default; opt-in per dep |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using workspace:* then publishing as-is | pnpm automatically replaces workspace:* with real versions on publish |
Forgetting to list directory in packages: | Every package directory must match a glob in pnpm-workspace.yaml |
Using npm install in workspace packages | Always use pnpm install from the workspace root |
| Hardcoding versions duplicated across packages | Use catalog: to centralize version definitions |
Running pnpm install inside a sub-package | Run from workspace root; use --filter to target packages |
Expecting --filter to match directory names | --filter matches package.json name field, not directory names |
Not escaping ! in zsh for exclude filters | Use \! or quote the filter: --filter="!foo" |
Using workspace: for non-workspace deps | workspace: protocol only works for packages defined in the workspace |
| Lifecycle scripts failing after pnpm 10 upgrade | pnpm 10 blocks scripts by default; add deps to allowBuilds in config |
Using pnpm deploy without inject-workspace-packages | Set inject-workspace-packages=true in .npmrc (required in pnpm 10) |
Delegation
- Workspace scaffolding: Use
Exploreagent to discover existing package structure - Dependency auditing: Use
Taskagent to check version consistency across packages
If the turborepo skill is available, delegate build orchestration, task caching, and CI optimization to it.If the changesets skill is available, delegate versioning, changelog generation, and npm publishing to it.References
- Workspace setup, workspace protocol, and catalogs
- Filtering packages for targeted commands
- Shared configuration across workspace packages
- Monorepo integration: pnpm + Turborepo + Changesets pipeline
Filtering
The --filter flag (alias -F) restricts pnpm commands to specific workspace packages. Filters can match by name, dependency relationship, directory, or git changes.
By Package Name
Match exact package names or use glob patterns:
pnpm --filter @myapp/web build
pnpm --filter "@myapp/*" build
pnpm --filter "*utils" testScope is optional if the name is unique in the workspace. If multiple packages share the same unscoped name (e.g., @myapp/core and @types/core), the filter matches nothing without the scope.
pnpm --filter web dev
pnpm --filter @myapp/web devBy Dependencies
Package and All Its Dependencies
Suffix with ... to include a package and everything it depends on:
pnpm --filter "web..." buildThis builds web and every package web depends on (direct and transitive).
Only Dependencies (Exclude the Package Itself)
Use ^... to select only the dependencies, not the package:
pnpm --filter "web^..." buildThis builds all dependencies of web but not web itself. Useful for ensuring dependencies are built before the consuming package.
By Dependents
Package and All Its Dependents
Prefix with ... to include a package and everything that depends on it:
pnpm --filter "...@myapp/ui" testThis tests @myapp/ui and every package that depends on it.
Only Dependents (Exclude the Package Itself)
Use ...^ to select only the dependents:
pnpm --filter "...^@myapp/ui" testThis tests all packages that depend on @myapp/ui but not @myapp/ui itself. Useful for verifying consumers still work after a change.
By Directory
Use ./path or {path} syntax to match packages by filesystem location:
pnpm --filter "./apps/web" build
pnpm --filter "{packages}" test
pnpm --filter "{apps}..." buildThe {path} syntax can combine with dependency/dependent operators:
pnpm --filter "...{packages}" test
pnpm --filter "{packages}..." build
pnpm --filter "...{packages}..." testBy Git Changes
Select packages that changed since a specific commit or branch using [ref]:
pnpm --filter "[origin/main]" test
pnpm --filter "[HEAD~3]" buildCombine with dependency/dependent operators to also test affected packages:
pnpm --filter "...[origin/main]" test
pnpm --filter "[origin/main]..." buildCombine with directory filters:
pnpm --filter "{packages}[origin/main]" testTest Pattern
Use --test-pattern with change-based filters to only run tests when specific files changed:
pnpm --filter "...[origin/main]" --test-pattern "tests/*" testExcluding Packages
Prepend ! to exclude packages from the selection:
pnpm --filter "!@myapp/web" build
pnpm --filter "@myapp/*" --filter "!@myapp/web" build
pnpm --filter "!./apps/web" buildIn zsh, escape the ! or use quotes:
pnpm --filter="\!@myapp/web" build
pnpm --filter="!@myapp/web" buildCombining Filters
Use multiple --filter flags to combine selectors. All matches are unioned:
pnpm --filter "...@myapp/ui" --filter "@myapp/web" --filter "utils..." testCommon Commands with Filters
Run Scripts
pnpm --filter @myapp/web dev
pnpm --filter @myapp/web build
pnpm --filter "@myapp/*" lintInstall Dependencies
pnpm --filter @myapp/web add react
pnpm --filter @myapp/web add -D vitest
pnpm --filter @myapp/web remove lodashRecursive Commands
Run a script in all workspace packages:
pnpm -r run build
pnpm -r run test
pnpm -r run lintCombine recursive with filters:
pnpm -r --filter "./packages/*" run buildExecute Binaries
pnpm --filter @myapp/web exec vitest run
pnpm --filter @myapp/web dlx create-next-appCI/CD Patterns
Build Only Changed Packages
pnpm --filter "...[origin/main]" run buildTest Changed Packages and Their Dependents
pnpm --filter "...[origin/main]" run testBuild a Package and Its Dependencies in Order
pnpm --filter "web..." run buildpnpm respects the topological dependency order, building dependencies before their consumers.
Lint Everything Except Documentation
pnpm --filter "!@myapp/docs" -r run lintFilter Cheat Sheet
| Filter | Selects |
|---|---|
--filter foo | Package named foo |
--filter "foo..." | foo + all its dependencies |
--filter "foo^..." | Only dependencies of foo |
--filter "...foo" | foo + all its dependents |
--filter "...^foo" | Only dependents of foo |
--filter "./apps/web" | Package at that directory path |
--filter "{packages}" | All packages under packages/ |
--filter "[origin/main]" | Packages changed since origin/main |
--filter "!foo" | Everything except foo |
--filter "@scope/*" | All packages matching the glob |
--filter "...[main]..." | Changed packages + their deps + their dependents |
Monorepo Integration
How pnpm workspaces, Turborepo, and Changesets fit together as a stack.
Tool Responsibilities
| Tool | Role | Scope |
|---|---|---|
| pnpm workspaces | Package linking, dependency management | pnpm-workspace.yaml, workspace: protocol, catalogs, lockfile |
| Turborepo | Task orchestration, caching | turbo.json, dependsOn, build/test/lint pipelines |
| Changesets | Versioning, changelog, publishing | .changeset/, semver decisions, npm publish |
When to Use What
Setting up the monorepo?
├── Defining packages and linking → pnpm workspaces
├── Build/test/lint pipelines → Turborepo
└── Versioning and publishing → Changesets
Day-to-day development?
├── Installing dependencies → pnpm install (root)
├── Running tasks → turbo run build/test/lint
├── Adding a changeset → changeset add
└── Filtering commands → --filter (both pnpm and turbo)
Releasing packages?
├── Consuming changesets → changeset version
├── Building in dependency order → turbo run build
├── Publishing to npm → changeset publish
└── CI automation → changesets/actionEnd-to-End Release Pipeline
Root package.json
{
"private": true,
"packageManager": "pnpm@10.29.0",
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"changeset": "changeset",
"version-packages": "changeset version",
"ci:publish": "turbo run build && changeset publish"
}
}Release Workflow
# 1. Developer adds changeset during feature work
pnpm changeset add
# 2. Changeset is committed with the PR
git add .changeset/ && git commit -m "feat: add feature"
# 3. CI merges PR — changesets/action detects pending changesets
# Action opens a "Version Packages" PR with bumped versions
# 4. Merge the Version Packages PR
# Action runs ci:publish: turbo builds in order, then changesets publishes
# 5. Tags are pushed automaticallyGitHub Actions CI
name: Release
on:
push:
branches: [main]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: changesets/action@v1
with:
publish: pnpm ci:publish
version: pnpm changeset version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Changesets Config for pnpm + Turborepo
{
"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }],
"commit": false,
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"bumpVersionsWithWorkspaceProtocolOnly": true
}bumpVersionsWithWorkspaceProtocolOnly: true prevents bumping bare semver ranges — only workspace:*, workspace:^, and workspace:~ ranges trigger dependent bumps.
Docker Deployment
Two approaches for containerizing monorepo apps: pnpm deploy and turbo prune.
pnpm deploy (Recommended)
Copies a package and its isolated node_modules to a target directory. Requires inject-workspace-packages=true in .npmrc (pnpm 10+).
FROM node:24-slim AS base
RUN corepack enable
FROM base AS builder
WORKDIR /app
COPY . .
RUN pnpm install --frozen-lockfile
RUN pnpm --filter=@myapp/web --prod deploy /prod/web
FROM base
WORKDIR /app
COPY --from=builder /prod/web .
EXPOSE 3000
CMD ["node", "dist/index.mjs"]Use --legacy flag or force-legacy-deploy: true to skip the inject-workspace-packages requirement.
turbo prune --docker
Creates a pruned monorepo slice optimized for Docker layer caching. Splits output into json/ (package.json files only) and full/ (complete source).
FROM node:24-slim AS base
RUN corepack enable
FROM base AS pruner
WORKDIR /app
COPY . .
RUN pnpm dlx turbo prune @myapp/web --docker
FROM base AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build --filter=@myapp/web
FROM base
WORKDIR /app
COPY --from=installer /app/apps/web/dist ./dist
COPY --from=installer /app/apps/web/package.json .
EXPOSE 3000
CMD ["node", "dist/index.mjs"]Which to Choose
| Factor | pnpm deploy | turbo prune |
|---|---|---|
| Output | Self-contained directory | Pruned monorepo structure |
| Layer caching | Single COPY layer | Separate json/full layers |
| Build step | Build before deploy | Build inside Docker |
| Lockfile | Generates dedicated lockfile | Prunes existing lockfile |
| Best for | Simple apps, production images | Complex builds, CI caching |
Filtering: pnpm vs Turborepo
Both tools support --filter but with different syntax and behavior.
| Pattern | pnpm | Turborepo |
|---|---|---|
| By name | --filter "web" | --filter=web |
| With dependencies | --filter "web..." | --filter=web... |
| With dependents | --filter "...web" | --filter=...web |
| By directory | --filter "./apps/*" | --filter=./apps/* |
| Git changes | --filter "[origin/main]" | --affected |
| Exclude | --filter "\!web" | --filter=!web |
When to use which:
- Use pnpm `--filter` for package management:
pnpm --filter web add react - Use Turborepo `--filter` for task execution:
turbo run build --filter=web
Workspace Protocol + Changesets
When Changesets versions a package, it updates workspace:^ and workspace:~ ranges in consuming packages. The updateInternalDependencies: "patch" config controls the bump cascade.
| Protocol | Before version | After @myapp/ui bumps to 2.0.0 |
|---|---|---|
workspace:* | Always latest local | No version file change |
workspace:^ | Caret range on publish | Updated to ^2.0.0 on publish |
workspace:~ | Tilde range on publish | Updated to ~2.0.0 on publish |
Shared Configs
Centralizing configuration in a pnpm workspace avoids duplication and ensures consistency across packages. Common shared configs include TypeScript, ESLint, and Prettier.
Shared TypeScript Configuration
Base tsconfig Package
Create a shared config package that other packages extend:
packages/
├── tsconfig/
│ ├── package.json
│ ├── base.json
│ ├── react.json
│ └── node.json
├── web/
│ ├── package.json
│ └── tsconfig.json
└── api/
├── package.json
└── tsconfig.jsonThe shared package package.json:
{
"name": "@myapp/tsconfig",
"version": "1.0.0",
"private": true,
"files": ["*.json"]
}Base Configuration
A strict base that all packages inherit:
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"exclude": ["node_modules", "dist"]
}React Configuration
Extends the base with JSX and DOM settings:
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}Node Configuration
Extends the base with Node.js-specific settings:
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022"],
"module": "Node16",
"moduleResolution": "Node16"
}
}Consuming the Shared Config
Add the config package as a workspace dependency:
{
"name": "@myapp/web",
"devDependencies": {
"@myapp/tsconfig": "workspace:*"
}
}Extend from the shared config in tsconfig.json:
{
"extends": "@myapp/tsconfig/react.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}For the Node API package:
{
"extends": "@myapp/tsconfig/node.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}Shared ESLint Configuration
ESLint Config Package
Create a shared ESLint config as a workspace package:
packages/
├── eslint-config/
│ ├── package.json
│ └── index.js
├── web/
│ ├── package.json
│ └── eslint.config.js
└── api/
├── package.json
└── eslint.config.jsThe config package package.json:
{
"name": "@myapp/eslint-config",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": {
".": "./index.js"
},
"dependencies": {
"@eslint/js": "^9.0.0",
"typescript-eslint": "^8.0.0"
},
"devDependencies": {
"@myapp/tsconfig": "workspace:*"
}
}Flat Config (ESLint v9+)
The shared config exports an array of config objects:
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strict,
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': 'error',
},
},
{
ignores: ['**/dist/**', '**/node_modules/**'],
},
);Consuming ESLint Config
Add the dependency:
{
"name": "@myapp/web",
"devDependencies": {
"@myapp/eslint-config": "workspace:*"
}
}Spread the shared config and add package-specific overrides:
import baseConfig from '@myapp/eslint-config';
export default [
...baseConfig,
{
rules: {
'no-console': 'warn',
},
},
];Shared Prettier Configuration
Root Prettier Config
Prettier config at the workspace root applies to all packages. No shared package needed:
{
"singleQuote": true,
"semi": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80
}Place this in the workspace root as .prettierrc or prettier.config.js.
Prettier Config as a Package
For more complex setups, create a shared package:
{
"name": "@myapp/prettier-config",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": {
".": "./index.js"
}
}export default {
singleQuote: true,
semi: true,
tabWidth: 2,
trailingComma: 'all',
printWidth: 80,
plugins: ['prettier-plugin-tailwindcss'],
};Reference in each package's package.json:
{
"name": "@myapp/web",
"prettier": "@myapp/prettier-config",
"devDependencies": {
"@myapp/prettier-config": "workspace:*"
}
}Root package.json Scripts
Define workspace-wide scripts in the root package.json:
{
"name": "@myapp/root",
"private": true,
"scripts": {
"build": "pnpm -r run build",
"dev": "pnpm --filter @myapp/web dev",
"lint": "pnpm -r run lint",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "pnpm -r run typecheck",
"test": "pnpm -r run test",
"clean": "pnpm -r exec rm -rf dist node_modules/.cache"
}
}.npmrc Workspace Settings
Common workspace-related .npmrc settings at the workspace root:
link-workspace-packages=false
prefer-workspace-packages=true
shared-workspace-lockfile=true
save-workspace-protocol=rolling| Setting | Recommended | Description |
|---|---|---|
link-workspace-packages | false | Require explicit workspace: protocol |
prefer-workspace-packages | true | Prefer local packages over registry |
shared-workspace-lockfile | true | Single lockfile at workspace root (default) |
save-workspace-protocol | rolling | Auto-add workspace:^ when installing local packages |
Workspace Setup
pnpm-workspace.yaml
The pnpm-workspace.yaml file at the repository root defines which directories contain workspace packages. Globs match directories that contain a package.json.
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'Nested globs and exclusion patterns are supported:
packages:
- 'packages/**'
- 'apps/**'
- '!**/test/**'
- '!**/__fixtures__/**'A typical monorepo directory structure:
workspace-root/
├── pnpm-workspace.yaml
├── package.json
├── .npmrc
├── apps/
│ ├── web/
│ │ └── package.json
│ └── mobile/
│ └── package.json
├── packages/
│ ├── ui/
│ │ └── package.json
│ └── utils/
│ └── package.json
└── tools/
└── scripts/
└── package.jsonWorkspace Protocol
The workspace: protocol ensures dependencies resolve to local workspace packages rather than fetching from the registry.
Basic Linking
Use workspace:* to link to any version of a local package:
{
"name": "@myapp/web",
"dependencies": {
"@myapp/ui": "workspace:*",
"@myapp/utils": "workspace:*"
}
}Version Range Linking
Specify a semver range to enforce version constraints. Installation fails if the local package version does not satisfy the range:
{
"name": "@myapp/web",
"dependencies": {
"@myapp/ui": "workspace:^2.0.0",
"@myapp/utils": "workspace:~1.5.0"
}
}Relative Path Linking
Link by relative path when packages are not in standard workspace directories:
{
"name": "@myapp/web",
"dependencies": {
"@myapp/shared": "workspace:../shared"
}
}Alias Linking
Create an alias for a workspace package:
{
"name": "@myapp/web",
"dependencies": {
"core-lib": "workspace:@myapp/core@*"
}
}Publishing Behavior
When running pnpm publish or pnpm pack, workspace protocol specifiers are automatically replaced with real versions:
| Workspace Specifier | Published As |
|---|---|
workspace:* | 1.2.3 |
workspace:^ | ^1.2.3 |
workspace:~ | ~1.2.3 |
workspace:^1.0.0 | ^1.0.0 |
This replacement happens automatically; no manual version updates are needed before publishing.
link-workspace-packages
The .npmrc setting link-workspace-packages controls automatic linking behavior:
# .npmrc
link-workspace-packages = true| Value | Behavior |
|---|---|
true | Local packages matching version ranges are linked automatically |
false | Only workspace: protocol triggers local linking (recommended) |
deep | Links local packages to subdependencies as well |
Setting link-workspace-packages=false with explicit workspace: protocol is the recommended approach. It makes dependency relationships explicit and avoids accidental linking.
Catalogs
Catalogs define dependency versions in pnpm-workspace.yaml so all workspace packages share consistent versions.
Default Catalog
The catalog: key defines a default set of versions:
packages:
- 'apps/*'
- 'packages/*'
catalog:
react: ^18.3.1
react-dom: ^18.3.1
typescript: ^5.6.0
vite: ^6.0.0Reference the default catalog in any workspace package.json:
{
"name": "@myapp/web",
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vite": "catalog:"
}
}Named Catalogs
Use catalogs: (plural) for multiple version sets:
catalogs:
react18:
react: ^18.3.1
react-dom: ^18.3.1
'@types/react': ^18.3.0
react17:
react: ^17.0.2
react-dom: ^17.0.2
'@types/react': ^17.0.0
testing:
vitest: ^2.0.0
'@testing-library/react': ^16.0.0
'@testing-library/jest-dom': ^6.0.0Reference named catalogs by name:
{
"name": "@myapp/web",
"dependencies": {
"react": "catalog:react18",
"react-dom": "catalog:react18"
},
"devDependencies": {
"vitest": "catalog:testing",
"@testing-library/react": "catalog:testing"
}
}Publishing with Catalogs
Like workspace protocol, catalog: specifiers are replaced with actual version ranges on pnpm publish:
{
"dependencies": {
"react": "catalog:react18"
}
}Becomes after publish:
{
"dependencies": {
"react": "^18.3.1"
}
}Combining Default and Named Catalogs
Both catalog (singular, default) and catalogs (plural, named) can coexist:
packages:
- 'apps/*'
- 'packages/*'
catalog:
typescript: ^5.6.0
prettier: ^3.4.0
eslint: ^9.0.0
catalogs:
react18:
react: ^18.3.1
react-dom: ^18.3.1
react17:
react: ^17.0.2
react-dom: ^17.0.2{
"name": "@myapp/web",
"dependencies": {
"react": "catalog:react18"
},
"devDependencies": {
"typescript": "catalog:",
"prettier": "catalog:"
}
}inject-workspace-packages
Hard-links all local workspace dependencies instead of symlinking them. Required for pnpm deploy in pnpm 10+:
# .npmrc
inject-workspace-packages=trueWhen enabled, workspace packages are injected as if they were regular dependencies (hard-linked from the store). This is useful for bundlers that do not follow symlinks and for pnpm deploy to create proper lockfiles for deployed projects.
Shared Lockfile
pnpm workspaces use a single pnpm-lock.yaml at the workspace root. This provides:
- Consistent dependency resolution across all packages
- Faster installs through shared dependency deduplication
- Single source of truth for the dependency tree
Always run pnpm install from the workspace root to keep the lockfile in sync.