shared-monorepo-pnpm-workspaces
pnpm workspace protocol, filtering, catalogs, shared dependencies, publishing, and CI/CD for monorepo management
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/shared-monorepo-pnpm-workspaces/skills/shared-monorepo-pnpm-workspaces
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
pnpm Workspaces for Monorepo Management
Quick Guide: pnpm 10.x workspaces for monorepo management.
pnpm-workspace.yamldefines workspace packages.workspace:*protocol for internal linking.catalog:protocol for dependency version synchronization.--filterfor targeted commands. Sharedtsconfigand tooling config across packages. Changesets for versioning and publishing. Strict dependency isolation by default.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use workspace:* protocol for ALL internal package dependencies -- never hardcode versions)
(You MUST use --frozen-lockfile in CI -- pnpm enables this by default in CI environments)
(You MUST define workspace packages in pnpm-workspace.yaml at the repository root)
(You MUST put pnpm-specific settings in pnpm-workspace.yaml -- NOT .npmrc (pnpm v10 change))
(You MUST use catalog: protocol when sharing dependency versions across 3+ packages)
(You MUST use --filter for targeted commands instead of pnpm -r when only specific packages changed)
</critical_requirements>
Auto-detection: pnpm-workspace.yaml, pnpm workspaces, workspace protocol, workspace:*, catalog:, pnpm filter, pnpm recursive, pnpm monorepo, pnpm-lock.yaml, .npmrc pnpm, pnpm catalogs, pnpm publish
When to use:
- Setting up a monorepo with pnpm workspaces
- Configuring
pnpm-workspace.yamlfor workspace package discovery - Linking internal packages with
workspace:*protocol - Synchronizing dependency versions with
catalog:protocol - Running scripts across workspaces with
--filteror-r - Publishing packages from a pnpm workspace
- Setting up CI/CD pipelines with pnpm caching
- Sharing TypeScript, ESLint, or Prettier config across workspace packages
- Migrating from npm/yarn workspaces to pnpm
When NOT to use:
- Single-package projects with no shared code
- Projects using Bun or Yarn as their package manager
- Projects that need npm compatibility exclusively (e.g., npm workspaces)
- Task orchestration logic (use a dedicated task runner on top of pnpm)
Key patterns covered:
pnpm-workspace.yamlsetup and workspace package discovery- Workspace protocol (
workspace:*,workspace:^,workspace:~) - Catalogs for dependency version synchronization
- Filtering commands (
--filter,-F, glob patterns, dependency selectors) - Running scripts across workspaces (
-r,--parallel,--workspace-concurrency) - Settings in
pnpm-workspace.yaml(v10: settings moved from.npmrc) - Publishing with
publishConfigand changesets - CI/CD with GitHub Actions, caching, and
--frozen-lockfile - Shared TypeScript configuration patterns
- Monorepo directory structure conventions
Detailed resources:
- Core Setup -- pnpm-workspace.yaml, .npmrc, directory structure, settings
- Shared Packages -- workspace protocol, catalogs, TypeScript/ESLint config
- Scripts & Filtering -- --filter, recursive execution, dependency management
- Publishing & Versioning -- changesets, publishConfig, Docker
- CI/CD Pipelines -- GitHub Actions, automated release
- Quick Command Reference -- condensed lookup table
<decision_framework>
Decision Framework
When to Use Catalogs vs Direct Versions
Does 3+ packages use this dependency?
YES -> Use catalog: protocol
NO -> Direct version is fine
Will this dependency version be updated frequently?
YES -> Use catalog: (one-line update)
NO -> Direct version is acceptable
workspace:* vs workspace:^ vs workspace:~
Are you publishing packages to npm?
NO -> Use workspace:* (exact local linking, version irrelevant)
YES -> Do consumers need flexible version ranges?
YES -> workspace:^ (caret range on publish)
NO -> workspace:* (exact version on publish)
When to Use --filter vs -r
Running a command in CI?
YES -> Use --filter "...[origin/main]" (only affected packages)
NO -> Running locally?
ALL packages -> pnpm -r <cmd>
ONE package -> pnpm --filter <name> <cmd>
Is the command order-dependent (build)?
YES -> Use -r (topological order) or --filter with ...
NO -> Use --parallel for speed (lint, test)
pnpm vs npm vs Yarn Workspaces
Need strict dependency isolation?
YES -> pnpm (non-flat node_modules by default)
NO -> Any works
Need disk efficiency?
YES -> pnpm (content-addressable store)
NO -> Any works
Need zero-config PnP (no node_modules)?
YES -> Yarn Berry with PnP
NO -> pnpm or npm
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Hardcoded versions for internal packages instead of
workspace:*(breaks local linking, installs from registry) - Settings in
.npmrcthat should be inpnpm-workspace.yaml(silently ignored in pnpm v10) - Missing
pnpm-workspace.yamlat repo root (pnpm will not recognize workspace packages) - Running
pnpm installwithout--frozen-lockfilein CI (lockfile can mutate silently) - Using
--parallelfor build commands when packages depend on each other (race conditions)
Medium Priority Issues:
- Not using
catalog:when 3+ packages share the same dependency version (version drift) - Running
pnpm -r buildin CI instead of--filter "...[origin/main]"(wastes time) - Missing
private: trueon internal packages (risk of accidental npm publish) - Not configuring
allowBuilds(or the olderonlyBuiltDependencies) allowlist (blocks all install scripts in v10)
Common Mistakes:
- Mixing package managers (npm install in a pnpm workspace breaks the lockfile)
- Using
shamefullyHoist: trueas a quick fix instead of declaring missing dependencies properly - Forgetting
fetch-depth: 0in GitHub Actions checkout (breaks git-based change detection) - Running different pnpm versions locally vs CI (lockfile format incompatibility)
Gotchas & Edge Cases:
workspace:*is replaced with the actual version onpnpm publish-- this is expected behavior, not a bugcatalog:entries must match the dependency name exactly -- typos silently fall through--filter "...[origin/main]"requires git history -- usefetch-depth: 0or at minimumfetch-depth: 2- pnpm v10 blocks ALL lifecycle scripts by default -- use
pnpm approve-buildsto allowlist packages that needpostinstall(likeesbuild,sharp), or configureallowBuildsinpnpm-workspace.yaml injectWorkspacePackages: trueis required forpnpm deploy(or usepnpm deploy --legacyto bypass)- Circular workspace dependencies cause unpredictable script execution order -- use
disallowWorkspaceCycles: true saveWorkspaceProtocol: rolling(default) meanspnpm addauto-saves withworkspace:-- this is correct behavior
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use workspace:* protocol for ALL internal package dependencies -- never hardcode versions)
(You MUST use --frozen-lockfile in CI -- pnpm enables this by default in CI environments)
(You MUST define workspace packages in pnpm-workspace.yaml at the repository root)
(You MUST put pnpm-specific settings in pnpm-workspace.yaml -- NOT .npmrc (pnpm v10 change))
(You MUST use catalog: protocol when sharing dependency versions across 3+ packages)
(You MUST use --filter for targeted commands instead of pnpm -r when only specific packages changed)
Failure to follow these rules will cause broken dependency resolution, version drift, missed CI caching, and security vulnerabilities.
</critical_reminders>
Files (skills)
-
examples
-
ci.md 2.8 KB
# pnpm Workspaces -- CI/CD Pipeline Examples > GitHub Actions workflows for build, test, and automated release. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [core.md](core.md) -- Workspace initialization, pnpm-workspace.yaml, settings - [packages.md](packages.md) -- Shared packages, TypeScript config, workspace protocol - [scripts.md](scripts.md) -- Running scripts, filtering, dependency management - [publishing.md](publishing.md) -- Changesets, versioning, publishing, Docker --- ## GitHub Actions: Build + Test ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] jobs: ci: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" - name: Install run: pnpm install - name: Typecheck run: pnpm -r --parallel typecheck - name: Lint run: pnpm -r --parallel lint - name: Build (affected only) run: pnpm --filter "...[origin/main]" build - name: Test (affected only) run: pnpm --filter "...[origin/main]" test ``` **Why good:** `pnpm/action-setup@v4` handles pnpm installation, `cache: "pnpm"` in setup-node caches the store, `--frozen-lockfile` is automatic in CI (prevents lockfile mutations), `--filter "...[origin/main]"` only builds/tests changed packages, `fetch-depth: 0` enables git-based change detection --- ## GitHub Actions: Automated Release with Changesets ```yaml name: Release on: push: branches: [main] concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write pull-requests: write jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" registry-url: "https://registry.npmjs.org" - name: Install run: pnpm install - name: Build run: pnpm -r build - name: Create Release PR or Publish uses: changesets/action@v1 with: version: pnpm changeset version publish: pnpm publish -r --access=public env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ``` **Why good:** `concurrency` prevents duplicate runs, `permissions` grants write access for PR creation, `changesets/action` auto-creates version bump PRs and publishes on merge, pnpm store is cached between runs -
core.md 4.7 KB
# pnpm Workspaces -- Core Examples > Workspace initialization and configuration examples. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [packages.md](packages.md) -- Shared packages, TypeScript config, workspace protocol - [scripts.md](scripts.md) -- Running scripts, filtering, dependency management - [publishing.md](publishing.md) -- Changesets, versioning, publishing - [ci.md](ci.md) -- CI/CD pipelines, GitHub Actions, Docker --- ## Minimal Workspace ```yaml # pnpm-workspace.yaml packages: - "apps/*" - "packages/*" ``` ```json { "name": "my-monorepo", "private": true, "packageManager": "pnpm@10.32.1", "scripts": { "build": "pnpm -r build", "dev": "pnpm -r --parallel dev", "test": "pnpm -r test", "lint": "pnpm -r --parallel lint", "clean": "pnpm -r exec rm -rf dist node_modules" } } ``` **Why good:** `private: true` prevents accidental root publish, `packageManager` field ensures consistent pnpm version, glob patterns auto-discover packages --- ## Full Workspace with Catalogs and Settings ```yaml # pnpm-workspace.yaml packages: - "apps/*" - "packages/*" - "tools/*" # Dependency version catalog catalog: react: ^19.0.0 react-dom: ^19.0.0 typescript: ^5.7.0 vitest: ^3.0.0 zod: ^3.24.0 "@changesets/cli": ^2.27.0 # Workspace settings (moved from .npmrc in v10) linkWorkspacePackages: true saveWorkspaceProtocol: rolling disallowWorkspaceCycles: true strictPeerDependencies: true # Security: allowlist packages that can run install scripts # Use allowBuilds (preferred) or onlyBuiltDependencies (legacy) allowBuilds: esbuild: true sharp: true "@swc/core": true ``` **Why good:** Single file defines workspace structure, dependency versions, and pnpm settings. `disallowWorkspaceCycles: true` catches circular dependencies at install time. `allowBuilds` explicitly trusts only necessary packages to run install scripts. --- ## v10 Settings Migration In pnpm v10, most settings moved from `.npmrc` to `pnpm-workspace.yaml`. Only auth and registry settings remain in `.npmrc`. ```yaml # pnpm-workspace.yaml (pnpm v10+) packages: - "apps/*" - "packages/*" # Settings that were previously in .npmrc linkWorkspacePackages: true saveWorkspaceProtocol: rolling shamefullyHoist: false strictPeerDependencies: true ``` ```ini # .npmrc (pnpm v10+ -- ONLY auth and registry settings) //registry.npmjs.org/:_authToken=${NPM_TOKEN} @myorg:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} ``` **Why good:** Single source of truth for pnpm configuration, `.npmrc` only contains secrets and registry URLs, settings are version-controlled alongside workspace definition ```yaml # BAD: Settings in .npmrc (pnpm v10+) # These will be IGNORED by pnpm v10 # shamefully-hoist=true # link-workspace-packages=true ``` **Why bad:** pnpm v10 no longer reads non-auth settings from `.npmrc`, settings are silently ignored leading to unexpected behavior --- ## Recommended Directory Structure ``` my-monorepo/ apps/ web/ # Web application package.json tsconfig.json # Extends shared config api/ # API server package.json tsconfig.json packages/ ui/ # Shared UI components package.json tsconfig.json src/ index.ts # Barrel file with named exports types/ # Shared TypeScript types package.json tsconfig.json src/ index.ts config-typescript/ # Shared tsconfig base package.json tsconfig.base.json tsconfig.react.json config-eslint/ # Shared ESLint config package.json index.js pnpm-workspace.yaml # Workspace definition + settings package.json # Root package.json (workspace scripts) pnpm-lock.yaml # Single lockfile for all packages .npmrc # Auth and registry settings only (v10+) ``` --- ## Root package.json ```json { "name": "my-monorepo", "private": true, "scripts": { "build": "pnpm -r build", "dev": "pnpm -r --parallel dev", "test": "pnpm -r test", "lint": "pnpm -r --parallel lint", "clean": "pnpm -r exec rm -rf dist node_modules", "changeset": "changeset", "version-packages": "changeset version && pnpm install", "release": "pnpm build && pnpm publish -r --access=public" }, "devDependencies": { "@changesets/cli": "catalog:", "typescript": "catalog:" } } ``` **Why good:** Root scripts provide workspace-wide commands, `private: true` prevents accidental root publish, shared devDependencies hoisted to root reduce duplication See [reference.md](../reference.md) for a complete settings lookup table. -
packages.md 7.1 KB
# pnpm Workspaces -- Shared Packages Examples > Workspace protocol, internal packages, and shared configuration examples. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [core.md](core.md) -- Workspace initialization, pnpm-workspace.yaml, settings - [scripts.md](scripts.md) -- Running scripts, filtering, dependency management - [publishing.md](publishing.md) -- Changesets, versioning, publishing - [ci.md](ci.md) -- CI/CD pipelines, GitHub Actions, Docker --- ## Workspace Protocol ### Good: Internal Dependencies with workspace:\* ```json { "name": "@repo/web-app", "private": true, "dependencies": { "@repo/ui": "workspace:*", "@repo/types": "workspace:*", "@repo/api-client": "workspace:*", "react": "catalog:", "react-dom": "catalog:" }, "devDependencies": { "@repo/config-typescript": "workspace:*", "@repo/config-eslint": "workspace:*", "typescript": "catalog:" } } ``` **Why good:** `workspace:*` guarantees local linking for internal packages, `catalog:` centralizes external dependency versions, clear separation of internal vs external dependencies ### Bad: Hardcoded Versions for Internal Packages ```json { "name": "@repo/web-app", "dependencies": { "@repo/ui": "^1.0.0", "@repo/types": "1.2.3", "react": "^19.0.0" } } ``` **Why bad:** Hardcoded internal versions may pull from npm instead of local workspace, different packages may have different versions of the same internal dependency, manual version bumps required on every change ### Publishing: workspace:^ for Flexible Ranges ```json { "name": "@repo/ui", "version": "2.1.0", "dependencies": { "@repo/types": "workspace:^" } } ``` After `pnpm publish`: ```json { "name": "@repo/ui", "version": "2.1.0", "dependencies": { "@repo/types": "^2.1.0" } } ``` **When to use:** Publishing packages to npm where consumers need semver flexibility ### Aliasing ```json { "dependencies": { "ui-v2": "workspace:@repo/ui@*" } } ``` **When to use:** Migrating between package versions in the same workspace, running two versions of an internal package side by side --- ## Catalog Examples ### Default Catalog ```yaml # pnpm-workspace.yaml catalog: react: ^19.0.0 react-dom: ^19.0.0 typescript: ^5.7.0 vitest: ^3.0.0 zod: ^3.24.0 ``` ```json { "name": "@repo/web-app", "dependencies": { "react": "catalog:", "react-dom": "catalog:", "zod": "catalog:" }, "devDependencies": { "typescript": "catalog:", "vitest": "catalog:" } } ``` ### Named Catalogs for Version Migration ```yaml # pnpm-workspace.yaml catalogs: react18: react: ^18.3.1 react-dom: ^18.3.1 "@types/react": ^18.3.0 react19: react: ^19.0.0 react-dom: ^19.0.0 "@types/react": ^19.0.0 ``` ```json { "name": "@repo/legacy-dashboard", "dependencies": { "react": "catalog:react18", "react-dom": "catalog:react18" } } ``` ```json { "name": "@repo/new-app", "dependencies": { "react": "catalog:react19", "react-dom": "catalog:react19" } } ``` **Why good:** Named catalogs allow gradual migration between major versions, each app declares its target version explicitly, centralized management of both version tracks ### Strict Catalog Enforcement ```yaml # pnpm-workspace.yaml catalogMode: strict catalog: react: ^19.0.0 typescript: ^5.7.0 ``` With `catalogMode: strict`, this will **fail** on `pnpm install`: ```json { "dependencies": { "react": "^18.0.0" } } ``` Error: Package "react" must use `catalog:` protocol when `catalogMode` is `strict`. **When to use:** Enforce consistent versions across all packages with no exceptions --- ## Shared TypeScript Configuration ### Configuration Package ``` packages/config-typescript/ package.json tsconfig.base.json tsconfig.react.json tsconfig.node.json ``` ```json { "name": "@repo/config-typescript", "private": true, "exports": { "./base": "./tsconfig.base.json", "./react": "./tsconfig.react.json", "./node": "./tsconfig.node.json" } } ``` ### Base Configuration ```json { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "strict": true, "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "esModuleInterop": true, "isolatedModules": true, "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "verbatimModuleSyntax": true, "noUncheckedIndexedAccess": true } } ``` ### React Configuration (extends base) ```json { "extends": "./tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "lib": ["DOM", "DOM.Iterable", "ES2022"], "noEmit": true } } ``` ### Node.js Configuration (extends base) ```json { "extends": "./tsconfig.base.json", "compilerOptions": { "module": "Node16", "moduleResolution": "Node16", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src" } } ``` ### Consumer Usage ```json { "extends": "@repo/config-typescript/react", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules", "dist"] } ``` **Why good:** Consistent TypeScript settings across all packages, base config with strict options, variants for different environments (browser vs node), consumer packages only add project-specific paths --- ## Shared ESLint Configuration ### Configuration Package ```json { "name": "@repo/config-eslint", "private": true, "dependencies": { "@repo/config-typescript": "workspace:*" }, "exports": { ".": "./index.js", "./react": "./react.js" } } ``` ### Consumer Usage (flat config) ```js // apps/web/eslint.config.js import baseConfig from "@repo/config-eslint"; import reactConfig from "@repo/config-eslint/react"; export default [...baseConfig, ...reactConfig]; ``` --- ## Internal Package Examples ### Shared UI Package ```json { "name": "@repo/ui", "version": "1.0.0", "private": true, "sideEffects": false, "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" }, "./button": { "types": "./src/components/button/index.ts", "default": "./src/components/button/index.ts" } }, "dependencies": { "@repo/types": "workspace:*" }, "peerDependencies": { "react": "catalog:", "react-dom": "catalog:" }, "devDependencies": { "@repo/config-typescript": "workspace:*", "typescript": "catalog:" } } ``` **Why good:** `exports` defines explicit public API (prevents internal path imports), `sideEffects: false` enables tree-shaking, React in `peerDependencies` (not dependencies) prevents version duplication, `private: true` prevents accidental npm publish, source exports during development for fast HMR ### Shared Types Package ```json { "name": "@repo/types", "version": "1.0.0", "private": true, "sideEffects": false, "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" } }, "devDependencies": { "@repo/config-typescript": "workspace:*", "typescript": "catalog:" } } ``` -
pnpm-workspaces.md 15.3 KB
# pnpm Workspaces - Practical Examples > Practical examples for pnpm workspace setup, filtering, catalogs, publishing, CI pipelines, and shared configuration. See [../SKILL.md](../SKILL.md) for core concepts and [../reference.md](../reference.md) for quick command reference. --- ## Complete Workspace Setup ### Minimal Workspace ```yaml # pnpm-workspace.yaml packages: - "apps/*" - "packages/*" ``` ```json { "name": "my-monorepo", "private": true, "packageManager": "pnpm@10.32.1", "scripts": { "build": "pnpm -r build", "dev": "pnpm -r --parallel dev", "test": "pnpm -r test", "lint": "pnpm -r --parallel lint", "clean": "pnpm -r exec rm -rf dist node_modules" } } ``` ### Full Workspace with Catalogs and Settings ```yaml # pnpm-workspace.yaml packages: - "apps/*" - "packages/*" - "tools/*" # Dependency version catalog catalog: react: ^19.0.0 react-dom: ^19.0.0 typescript: ^5.7.0 vitest: ^3.0.0 zod: ^3.24.0 "@changesets/cli": ^2.27.0 # Workspace settings (moved from .npmrc in v10) linkWorkspacePackages: true saveWorkspaceProtocol: rolling disallowWorkspaceCycles: true strictPeerDependencies: true # Security: allowlist packages that need install scripts onlyBuiltDependencies: - esbuild - sharp - "@swc/core" ``` **Why good:** Single file defines workspace structure, dependency versions, and pnpm settings. `disallowWorkspaceCycles: true` catches circular dependencies at install time. `onlyBuiltDependencies` explicitly trusts only necessary packages. --- ## Workspace Protocol Examples ### Good: Internal Dependencies with workspace:\* ```json { "name": "@repo/web-app", "private": true, "dependencies": { "@repo/ui": "workspace:*", "@repo/types": "workspace:*", "@repo/api-client": "workspace:*", "react": "catalog:", "react-dom": "catalog:" }, "devDependencies": { "@repo/config-typescript": "workspace:*", "@repo/config-eslint": "workspace:*", "typescript": "catalog:" } } ``` **Why good:** `workspace:*` guarantees local linking for internal packages, `catalog:` centralizes external dependency versions, clear separation of internal vs external dependencies ### Bad: Hardcoded Versions for Internal Packages ```json { "name": "@repo/web-app", "dependencies": { "@repo/ui": "^1.0.0", "@repo/types": "1.2.3", "react": "^19.0.0" } } ``` **Why bad:** Hardcoded internal versions may pull from npm instead of local workspace, different packages may have different versions of the same internal dependency, manual version bumps required on every change ### Publishing: workspace:^ for Flexible Ranges ```json { "name": "@repo/ui", "version": "2.1.0", "dependencies": { "@repo/types": "workspace:^" } } ``` After `pnpm publish`: ```json { "name": "@repo/ui", "version": "2.1.0", "dependencies": { "@repo/types": "^2.1.0" } } ``` **When to use:** Publishing packages to npm where consumers need semver flexibility --- ## Catalog Examples ### Default Catalog ```yaml # pnpm-workspace.yaml catalog: react: ^19.0.0 react-dom: ^19.0.0 next: ^15.0.0 typescript: ^5.7.0 vitest: ^3.0.0 zod: ^3.24.0 drizzle-orm: ^0.38.0 hono: ^4.7.0 ``` ```json { "name": "@repo/web-app", "dependencies": { "react": "catalog:", "react-dom": "catalog:", "next": "catalog:", "zod": "catalog:" }, "devDependencies": { "typescript": "catalog:", "vitest": "catalog:" } } ``` ### Named Catalogs for Version Migration ```yaml # pnpm-workspace.yaml catalogs: react18: react: ^18.3.1 react-dom: ^18.3.1 "@types/react": ^18.3.0 react19: react: ^19.0.0 react-dom: ^19.0.0 "@types/react": ^19.0.0 ``` ```json { "name": "@repo/legacy-dashboard", "dependencies": { "react": "catalog:react18", "react-dom": "catalog:react18" } } ``` ```json { "name": "@repo/new-app", "dependencies": { "react": "catalog:react19", "react-dom": "catalog:react19" } } ``` **Why good:** Named catalogs allow gradual migration between major versions, each app declares its target version explicitly, centralized management of both version tracks ### Strict Catalog Enforcement ```yaml # pnpm-workspace.yaml catalogMode: strict catalog: react: ^19.0.0 typescript: ^5.7.0 ``` With `catalogMode: strict`, this will **fail** on `pnpm install`: ```json { "dependencies": { "react": "^18.0.0" } } ``` Error: Package "react" must use `catalog:` protocol when `catalogMode` is `strict`. **When to use:** Enforce consistent versions across all packages with no exceptions --- ## Filtering Command Examples ### Common Development Workflows ```bash # Start dev server for a specific app pnpm --filter @repo/web-app dev # Build a package and all its dependencies pnpm --filter "@repo/web-app..." build # Run tests for changed packages since main branch pnpm --filter "...[origin/main]" test # Lint everything except docs pnpm --filter "!@repo/docs" --filter "@repo/*" lint # Add a dependency to a specific package pnpm --filter @repo/api-server add hono # Add a workspace dependency pnpm --filter @repo/web-app add @repo/ui --workspace # Remove a dependency from a package pnpm --filter @repo/web-app remove lodash ``` ### CI-Optimized Filtering ```bash # Build only changed packages and their dependents pnpm --filter "...[origin/main]..." build # Test changed packages (ignore README changes) pnpm --filter "...[origin/main]" \ --changed-files-ignore-pattern="**/*.md" \ test # Type-check only packages in the packages/ directory pnpm --filter "./packages/**" typecheck # Build with failure on no matches (catches filter typos) pnpm --filter "@repo/web-app" --fail-if-no-match build ``` ### Dependency Graph Exploration ```bash # See what depends on @repo/types pnpm --filter "...@repo/types" list --depth 0 # See what @repo/web-app depends on pnpm --filter "@repo/web-app..." list --depth 0 # List all workspace packages pnpm -r list --depth -1 ``` --- ## Shared TypeScript Configuration ### Configuration Package ``` packages/config-typescript/ package.json tsconfig.base.json tsconfig.react.json tsconfig.node.json ``` ```json { "name": "@repo/config-typescript", "private": true, "exports": { "./base": "./tsconfig.base.json", "./react": "./tsconfig.react.json", "./node": "./tsconfig.node.json" } } ``` ### Base Configuration ```json { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "strict": true, "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "esModuleInterop": true, "isolatedModules": true, "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "verbatimModuleSyntax": true, "noUncheckedIndexedAccess": true } } ``` ### React Configuration (extends base) ```json { "extends": "./tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "lib": ["DOM", "DOM.Iterable", "ES2022"], "noEmit": true } } ``` ### Node.js Configuration (extends base) ```json { "extends": "./tsconfig.base.json", "compilerOptions": { "module": "Node16", "moduleResolution": "Node16", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src" } } ``` ### Consumer Usage ```json { "extends": "@repo/config-typescript/react", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules", "dist"] } ``` **Why good:** Consistent TypeScript settings across all packages, base config with strict options, variants for different environments (browser vs node), consumer packages only add project-specific paths --- ## Shared ESLint Configuration ### Configuration Package ```json { "name": "@repo/config-eslint", "private": true, "dependencies": { "@repo/config-typescript": "workspace:*" }, "exports": { ".": "./index.js", "./react": "./react.js" } } ``` ### Consumer Usage (flat config) ```js // apps/web/eslint.config.js import baseConfig from "@repo/config-eslint"; import reactConfig from "@repo/config-eslint/react"; export default [...baseConfig, ...reactConfig]; ``` --- ## Publishing Workflow with Changesets ### Setup ```bash # Install changesets in workspace root pnpm add -Dw @changesets/cli # Initialize changesets pnpm changeset init ``` ### Changesets Configuration ```json { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [["@repo/ui", "@repo/types"]], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["@repo/web-app", "@repo/api-server"] } ``` **Key options:** - `linked`: Packages that should always share the same version - `access`: `"public"` for scoped packages on npm - `ignore`: Private packages that should not be versioned/published - `updateInternalDependencies`: How to bump internal dep references ### Development Workflow ```bash # 1. Make code changes across packages # 2. Create a changeset describing the change pnpm changeset # Interactive prompt: select packages, bump type, description # 3. Commit the changeset file # .changeset/cool-dogs-dance.md gets committed with your PR # 4. When ready to release (usually automated): pnpm changeset version # Bumps versions, generates changelogs pnpm install # Update lockfile pnpm publish -r # Publish to npm ``` ### Root package.json Scripts ```json { "scripts": { "changeset": "changeset", "version-packages": "changeset version && pnpm install", "release": "pnpm build && pnpm publish -r --access=public" } } ``` --- ## CI/CD Pipeline Examples ### GitHub Actions: Build + Test ```yaml name: CI on: push: branches: [main] pull_request: branches: [main] jobs: ci: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" - name: Install run: pnpm install - name: Typecheck run: pnpm -r --parallel typecheck - name: Lint run: pnpm -r --parallel lint - name: Build (affected only) run: pnpm --filter "...[origin/main]" build - name: Test (affected only) run: pnpm --filter "...[origin/main]" test ``` ### GitHub Actions: Automated Release with Changesets ```yaml name: Release on: push: branches: [main] concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write pull-requests: write jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" registry-url: "https://registry.npmjs.org" - name: Install run: pnpm install - name: Build run: pnpm -r build - name: Create Release PR or Publish uses: changesets/action@v1 with: version: pnpm changeset version publish: pnpm publish -r --access=public env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ``` **Why good:** `concurrency` prevents duplicate runs, `permissions` grants write access for PR creation, `changesets/action` auto-creates version bump PRs and publishes on merge, pnpm store is cached between runs --- ## Internal Package Setup Example ### Shared UI Package ```json { "name": "@repo/ui", "version": "1.0.0", "private": true, "sideEffects": false, "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" }, "./button": { "types": "./src/components/button/index.ts", "default": "./src/components/button/index.ts" } }, "dependencies": { "@repo/types": "workspace:*" }, "peerDependencies": { "react": "catalog:", "react-dom": "catalog:" }, "devDependencies": { "@repo/config-typescript": "workspace:*", "typescript": "catalog:" } } ``` **Why good:** `exports` defines explicit public API (prevents internal path imports), `sideEffects: false` enables tree-shaking, React in `peerDependencies` (not dependencies) prevents version duplication, `private: true` prevents accidental npm publish, source exports during development for fast HMR ### Shared Types Package ```json { "name": "@repo/types", "version": "1.0.0", "private": true, "sideEffects": false, "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" } }, "devDependencies": { "@repo/config-typescript": "workspace:*", "typescript": "catalog:" } } ``` --- ## Docker with pnpm Workspaces ### Multi-Stage Dockerfile ```dockerfile # Stage 1: Install dependencies FROM node:22-alpine AS deps RUN corepack enable pnpm WORKDIR /app COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY apps/api/package.json ./apps/api/ COPY packages/types/package.json ./packages/types/ RUN pnpm install --frozen-lockfile # Stage 2: Build FROM node:22-alpine AS builder RUN corepack enable pnpm WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm --filter @repo/api build # Stage 3: Production FROM node:22-alpine AS runner RUN corepack enable pnpm WORKDIR /app COPY --from=builder /app/apps/api/dist ./dist COPY --from=builder /app/apps/api/package.json ./ ENV NODE_ENV=production CMD ["node", "dist/index.js"] ``` ### Using pnpm deploy (Recommended) ```yaml # pnpm-workspace.yaml injectWorkspacePackages: true # Required for pnpm deploy ``` ```dockerfile FROM node:22-alpine AS builder RUN corepack enable pnpm WORKDIR /app COPY . . RUN pnpm install --frozen-lockfile RUN pnpm --filter @repo/api build RUN pnpm --filter @repo/api deploy ./pruned FROM node:22-alpine AS runner WORKDIR /app COPY --from=builder /app/pruned . ENV NODE_ENV=production CMD ["node", "dist/index.js"] ``` **Why good:** `pnpm deploy` creates a standalone directory with only the production dependencies for a specific package, dramatically smaller Docker images, `injectWorkspacePackages: true` required for deploy to resolve workspace deps correctly --- ## Migration from npm/yarn ### Step-by-Step ```bash # 1. Remove old lockfile and node_modules rm -rf node_modules package-lock.json yarn.lock # 2. Create pnpm-workspace.yaml cat > pnpm-workspace.yaml << 'EOF' packages: - "apps/*" - "packages/*" EOF # 3. Set packageManager in root package.json # "packageManager": "pnpm@10.32.1" # 4. Install with pnpm pnpm install # 5. Convert internal dependencies to workspace:* # In each package.json, change "@repo/ui": "^1.0.0" to "@repo/ui": "workspace:*" # 6. Re-install to generate proper lockfile pnpm install ``` **Key differences from npm/yarn:** - pnpm creates strict `node_modules` (no phantom dependencies) - You may need to add missing dependency declarations that npm/yarn silently resolved - `shamefullyHoist: true` can ease migration but should be removed once deps are fixed -
publishing.md 4.9 KB
# pnpm Workspaces -- Publishing & Versioning Examples > Changesets, publishConfig, versioning, and Docker deployment examples. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [core.md](core.md) -- Workspace initialization, pnpm-workspace.yaml, settings - [packages.md](packages.md) -- Shared packages, TypeScript config, workspace protocol - [scripts.md](scripts.md) -- Running scripts, filtering, dependency management - [ci.md](ci.md) -- CI/CD pipelines, GitHub Actions, Docker --- ## publishConfig ```json { "name": "@repo/ui", "version": "1.0.0", "private": false, "main": "./src/index.ts", "publishConfig": { "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "access": "public" }, "scripts": { "build": "tsup src/index.ts --format esm --dts" } } ``` **Why good:** `main` points to source during development (fast HMR), `publishConfig.main` points to built output on publish, `access: public` required for scoped packages on npm --- ## Changesets Setup ```bash # Install changesets in workspace root pnpm add -Dw @changesets/cli # Initialize changesets pnpm changeset init ``` ### Configuration ```json { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [["@repo/ui", "@repo/types"]], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["@repo/web-app", "@repo/api-server"] } ``` **Key options:** - `linked`: Packages that should always share the same version - `access`: `"public"` for scoped packages on npm - `ignore`: Private packages that should not be versioned/published - `updateInternalDependencies`: How to bump internal dep references ### Development Workflow ```bash # 1. Make code changes across packages # 2. Create a changeset describing the change pnpm changeset # Interactive prompt: select packages, bump type, description # 3. Commit the changeset file # .changeset/cool-dogs-dance.md gets committed with your PR # 4. When ready to release (usually automated): pnpm changeset version # Bumps versions, generates changelogs pnpm install # Update lockfile pnpm publish -r # Publish to npm ``` ### Root package.json Scripts ```json { "scripts": { "changeset": "changeset", "version-packages": "changeset version && pnpm install", "release": "pnpm build && pnpm publish -r --access=public" } } ``` --- ## Docker with pnpm Workspaces ### Multi-Stage Dockerfile ```dockerfile # Stage 1: Install dependencies FROM node:22-alpine AS deps RUN corepack enable pnpm WORKDIR /app COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY apps/api/package.json ./apps/api/ COPY packages/types/package.json ./packages/types/ RUN pnpm install --frozen-lockfile # Stage 2: Build FROM node:22-alpine AS builder RUN corepack enable pnpm WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm --filter @repo/api build # Stage 3: Production FROM node:22-alpine AS runner RUN corepack enable pnpm WORKDIR /app COPY --from=builder /app/apps/api/dist ./dist COPY --from=builder /app/apps/api/package.json ./ ENV NODE_ENV=production CMD ["node", "dist/index.js"] ``` ### Using pnpm deploy (Recommended) ```yaml # pnpm-workspace.yaml injectWorkspacePackages: true # Required for pnpm deploy ``` ```dockerfile FROM node:22-alpine AS builder RUN corepack enable pnpm WORKDIR /app COPY . . RUN pnpm install --frozen-lockfile RUN pnpm --filter @repo/api build RUN pnpm --filter @repo/api deploy ./pruned FROM node:22-alpine AS runner WORKDIR /app COPY --from=builder /app/pruned . ENV NODE_ENV=production CMD ["node", "dist/index.js"] ``` **Why good:** `pnpm deploy` creates a standalone directory with only the production dependencies for a specific package, dramatically smaller Docker images, `injectWorkspacePackages: true` required for deploy to resolve workspace deps correctly. Use `pnpm deploy --legacy` if you cannot enable `injectWorkspacePackages` globally. --- ## Migration from npm/yarn ### Step-by-Step ```bash # 1. Remove old lockfile and node_modules rm -rf node_modules package-lock.json yarn.lock # 2. Create pnpm-workspace.yaml cat > pnpm-workspace.yaml << 'EOF' packages: - "apps/*" - "packages/*" EOF # 3. Set packageManager in root package.json # "packageManager": "pnpm@10.32.1" # 4. Install with pnpm pnpm install # 5. Convert internal dependencies to workspace:* # In each package.json, change "@repo/ui": "^1.0.0" to "@repo/ui": "workspace:*" # 6. Re-install to generate proper lockfile pnpm install ``` **Key differences from npm/yarn:** - pnpm creates strict `node_modules` (no phantom dependencies) - You may need to add missing dependency declarations that npm/yarn silently resolved - `shamefullyHoist: true` can ease migration but should be removed once deps are fixed -
scripts.md 4.3 KB
# pnpm Workspaces -- Scripts & Filtering Examples > Running scripts, filtering commands, and dependency management examples. Reference from [SKILL.md](../SKILL.md). **Related examples:** - [core.md](core.md) -- Workspace initialization, pnpm-workspace.yaml, settings - [packages.md](packages.md) -- Shared packages, TypeScript config, workspace protocol - [publishing.md](publishing.md) -- Changesets, versioning, publishing - [ci.md](ci.md) -- CI/CD pipelines, GitHub Actions, Docker --- ## Filtering Commands ### Common Development Workflows ```bash # Start dev server for a specific app pnpm --filter @repo/web-app dev # Build a package and all its dependencies pnpm --filter "@repo/web-app..." build # Run tests for changed packages since main branch pnpm --filter "...[origin/main]" test # Lint everything except docs pnpm --filter "!@repo/docs" --filter "@repo/*" lint # Add a dependency to a specific package pnpm --filter @repo/api-server add some-package # Add a workspace dependency pnpm --filter @repo/web-app add @repo/ui --workspace # Remove a dependency from a package pnpm --filter @repo/web-app remove lodash ``` ### Package Name Matching ```bash # Exact package name pnpm --filter @repo/web-app build # Glob pattern pnpm --filter "@repo/*" build # Short form pnpm -F @repo/web-app dev ``` ### Dependency and Dependent Selection ```bash # Package and ALL its dependencies (transitive) pnpm --filter "web-app..." build # Only dependencies of a package (excludes the package itself) pnpm --filter "web-app^..." build # Package and ALL its dependents (what depends on it) pnpm --filter "...@repo/ui" build # Only dependents (excludes the package itself) pnpm --filter "...^@repo/ui" build ``` ### Directory and Change-Based Filtering ```bash # All packages in a directory pnpm --filter "./packages/**" test # All packages changed since a git ref pnpm --filter "...[origin/main]" test # Changed packages and their dependents pnpm --filter "...[origin/main]..." build # Exclude a package pnpm --filter "!@repo/docs" build ``` ### CI-Optimized Filtering ```bash # Build only changed packages and their dependents pnpm --filter "...[origin/main]..." build # Test changed packages (ignore README changes) pnpm --filter "...[origin/main]" \ --changed-files-ignore-pattern="**/*.md" \ test # Type-check only packages in the packages/ directory pnpm --filter "./packages/**" typecheck # Build with failure on no matches (catches filter typos) pnpm --filter "@repo/web-app" --fail-if-no-match build ``` **Why good:** Targeted execution saves CI time, dependency-aware filtering ensures correct build order, change-based filtering only rebuilds what changed ### Dependency Graph Exploration ```bash # See what depends on @repo/types pnpm --filter "...@repo/types" list --depth 0 # See what @repo/web-app depends on pnpm --filter "@repo/web-app..." list --depth 0 # List all workspace packages pnpm -r list --depth -1 ``` --- ## Recursive Script Execution ### Topological vs Parallel ```bash # Run build in all packages (topological order -- respects dependency graph) pnpm -r build # Run in all packages including the root pnpm -r --include-workspace-root build # Run tests in parallel (ignores dependency graph) pnpm -r --parallel test # Control concurrency (4 packages at a time, topological order) pnpm -r --workspace-concurrency 4 build ``` ### Correct Ordering ```bash # CORRECT: Build in dependency order (packages build before their dependents) pnpm -r build # CORRECT: Tests can run in parallel (no build artifact dependencies) pnpm -r --parallel test # CORRECT: Lint can run in parallel (no cross-package dependencies) pnpm -r --parallel lint ``` ```bash # BAD: Building in parallel when packages depend on each other pnpm -r --parallel build ``` **Why bad:** Packages that depend on other packages may start building before their dependencies finish, causing build failures with missing modules --- ## Dependency Management ### Adding Dependencies ```bash # Add a dependency to a specific package pnpm --filter @repo/web-app add zod # Add a dev dependency to the workspace root pnpm add -Dw vitest # Add a workspace package as a dependency pnpm --filter @repo/web-app add @repo/ui --workspace # Update a dependency across all packages pnpm -r update typescript # Clean all packages pnpm -r exec rm -rf dist node_modules ```
-
-
reference.md 8.2 KB
# pnpm Workspaces Quick Reference > Quick reference for pnpm workspace commands, protocol syntax, and configuration options. See [SKILL.md](SKILL.md) for detailed patterns and [examples/](examples/) for practical examples. --- ## Workspace Protocol Syntax | Protocol | Development | On Publish | Use When | | ------------- | ----------- | ----------------------- | ---------------------------------------------- | | `workspace:*` | Local link | Exact version (`1.5.0`) | Internal packages (default choice) | | `workspace:^` | Local link | Caret range (`^1.5.0`) | Publishing packages that need flexible ranges | | `workspace:~` | Local link | Tilde range (`~1.5.0`) | Publishing packages with tight version control | --- ## Catalog Protocol Syntax | Syntax | Meaning | | ------------------- | ------------------------------------------------------------ | | `"catalog:"` | Use version from default `catalog:` in `pnpm-workspace.yaml` | | `"catalog:default"` | Explicit reference to default catalog | | `"catalog:<name>"` | Use version from named catalog in `catalogs:` | --- ## Filter Commands ### Package Selection ```bash pnpm --filter <package-name> <cmd> # Exact package pnpm --filter "@scope/*" <cmd> # Glob pattern pnpm -F <package-name> <cmd> # Short form ``` ### Dependency / Dependent Selection ```bash pnpm --filter "pkg..." <cmd> # Package + all its dependencies pnpm --filter "pkg^..." <cmd> # Only dependencies (excludes pkg) pnpm --filter "...pkg" <cmd> # Package + all its dependents pnpm --filter "...^pkg" <cmd> # Only dependents (excludes pkg) ``` ### Directory and Change-Based ```bash pnpm --filter "./packages/**" <cmd> # All packages in directory pnpm --filter "[origin/main]" <cmd> # Changed packages since ref pnpm --filter "...[origin/main]" <cmd> # Changed + their dependents ``` ### Exclusion ```bash pnpm --filter "!pkg-name" <cmd> # Exclude package pnpm --filter "!./lib" <cmd> # Exclude directory ``` ### Advanced Options ```bash --filter-prod # Omit devDependencies during selection --test-pattern="test/*" # Prevent dependent execution for test-only changes --changed-files-ignore-pattern="**/*.md" # Exclude files from change detection --fail-if-no-match # Error if no packages match ``` --- ## Script Execution ```bash pnpm -r <cmd> # Recursive, topological order pnpm -r --parallel <cmd> # Parallel, ignores dependency graph pnpm -r --workspace-concurrency 4 <cmd> # Topological, max 4 concurrent pnpm -r --include-workspace-root <cmd> # Include root package ``` --- ## Dependency Management ```bash pnpm add -Dw <pkg> # Add dev dep to workspace root pnpm --filter <name> add <pkg> # Add dep to specific package pnpm --filter <name> add <pkg> --workspace # Add workspace package as dep pnpm -r update <pkg> # Update dep across all packages pnpm -r exec rm -rf dist node_modules # Clean all packages ``` --- ## pnpm-workspace.yaml Settings (v10+) ### Workspace Definition ```yaml packages: - "apps/*" - "packages/*" ``` ### Dependency Resolution | Setting | Default | Purpose | | ------------------------- | --------- | --------------------------------------- | | `linkWorkspacePackages` | `false` | Link local packages to `node_modules` | | `saveWorkspaceProtocol` | `rolling` | Auto-save `workspace:` protocol | | `preferWorkspacePackages` | `false` | Prefer workspace packages over registry | | `disallowWorkspaceCycles` | `false` | Fail install on circular deps | | `ignoreWorkspaceCycles` | `false` | Suppress cycle warnings | ### Hoisting | Setting | Default | Purpose | | ------------------------ | ------- | ------------------------------------------- | | `shamefullyHoist` | `false` | Hoist everything to root (avoid this) | | `hoist` | `true` | Hoist to hidden `.pnpm/node_modules` | | `hoistPattern` | `["*"]` | Which packages to hoist | | `publicHoistPattern` | `[]` | Hoist to root `node_modules` | | `hoistWorkspacePackages` | `true` | Symlink workspace packages per hoist config | ### Injection | Setting | Default | Purpose | | ------------------------- | ------- | ----------------------------------------------------- | | `injectWorkspacePackages` | `false` | Hard-link workspace deps (required for `pnpm deploy`) | ### Catalogs | Setting | Default | Purpose | | ----------------------- | -------- | ---------------------------------------- | | `catalogMode` | `manual` | `strict` / `prefer` / `manual` | | `cleanupUnusedCatalogs` | `false` | Remove unused catalog entries on install | ### Security (v10) | Setting | Purpose | | ----------------------- | ------------------------------------------------------------------------ | | `allowBuilds` | Map-based allowlist for install scripts (preferred, per-package boolean) | | `onlyBuiltDependencies` | Array-based allowlist (legacy, still supported) | --- ## .npmrc (v10+) In pnpm v10, `.npmrc` should ONLY contain auth and registry settings: ```ini //registry.npmjs.org/:_authToken=${NPM_TOKEN} @myorg:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} ``` --- ## Build Script Approval ```bash pnpm approve-builds # Interactive approval of packages needing install scripts pnpm approve-builds --all # Approve all pending builds without prompting ``` --- ## Changesets Commands ```bash pnpm add -Dw @changesets/cli # Install pnpm changeset init # Initialize pnpm changeset # Create changeset (interactive) pnpm changeset version # Bump versions + changelogs pnpm install # Update lockfile after bumps pnpm publish -r # Publish updated packages pnpm publish -r --access=public # Publish scoped public packages ``` --- ## CI Checklist - [ ] Use `pnpm/action-setup@v4` with explicit version - [ ] Use `actions/setup-node@v4` with `cache: "pnpm"` - [ ] `--frozen-lockfile` is default in CI (do not disable) - [ ] Use `fetch-depth: 0` for change-based filtering - [ ] Use `--filter "...[origin/main]"` for affected-only builds - [ ] Pin pnpm version to match local development - [ ] Configure `NPM_TOKEN` secret for publishing - [ ] Configure `allowBuilds` (or `onlyBuiltDependencies`) for packages needing install scripts --- ## pnpm v10 Breaking Changes | Change | Migration | | ------------------------------------------------------ | --------------------------------------------------- | | Settings moved from `.npmrc` to `pnpm-workspace.yaml` | Move non-auth settings to YAML | | Lifecycle scripts blocked by default | Add `allowBuilds` map or run `pnpm approve-builds` | | `pnpm deploy` requires `injectWorkspacePackages: true` | Add setting to workspace config (or use `--legacy`) | | JSR support via `jsr:` protocol | Use for JSR packages | | `devEngines.runtime` support | Specify runtime versions in `package.json` | --- ## Resources **Official Documentation:** - pnpm Workspaces: https://pnpm.io/workspaces - pnpm Settings: https://pnpm.io/settings - pnpm Filtering: https://pnpm.io/filtering - pnpm Catalogs: https://pnpm.io/catalogs - pnpm CI: https://pnpm.io/continuous-integration - pnpm Changesets: https://pnpm.io/using-changesets **Tools:** - Changesets: https://github.com/changesets/changesets - pnpm GitHub Action: https://github.com/pnpm/action-setup -
SKILL.md 16.1 KB
--- name: shared-monorepo-pnpm-workspaces description: pnpm workspace protocol, filtering, catalogs, shared dependencies, publishing, and CI/CD for monorepo management --- # pnpm Workspaces for Monorepo Management > **Quick Guide:** pnpm 10.x workspaces for monorepo management. `pnpm-workspace.yaml` defines workspace packages. `workspace:*` protocol for internal linking. `catalog:` protocol for dependency version synchronization. `--filter` for targeted commands. Shared `tsconfig` and tooling config across packages. Changesets for versioning and publishing. Strict dependency isolation by default. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `workspace:*` protocol for ALL internal package dependencies -- never hardcode versions)** **(You MUST use `--frozen-lockfile` in CI -- pnpm enables this by default in CI environments)** **(You MUST define workspace packages in `pnpm-workspace.yaml` at the repository root)** **(You MUST put pnpm-specific settings in `pnpm-workspace.yaml` -- NOT `.npmrc` (pnpm v10 change))** **(You MUST use `catalog:` protocol when sharing dependency versions across 3+ packages)** **(You MUST use `--filter` for targeted commands instead of `pnpm -r` when only specific packages changed)** </critical_requirements> --- **Auto-detection:** pnpm-workspace.yaml, pnpm workspaces, workspace protocol, workspace:\*, catalog:, pnpm filter, pnpm recursive, pnpm monorepo, pnpm-lock.yaml, .npmrc pnpm, pnpm catalogs, pnpm publish **When to use:** - Setting up a monorepo with pnpm workspaces - Configuring `pnpm-workspace.yaml` for workspace package discovery - Linking internal packages with `workspace:*` protocol - Synchronizing dependency versions with `catalog:` protocol - Running scripts across workspaces with `--filter` or `-r` - Publishing packages from a pnpm workspace - Setting up CI/CD pipelines with pnpm caching - Sharing TypeScript, ESLint, or Prettier config across workspace packages - Migrating from npm/yarn workspaces to pnpm **When NOT to use:** - Single-package projects with no shared code - Projects using Bun or Yarn as their package manager - Projects that need npm compatibility exclusively (e.g., npm workspaces) - Task orchestration logic (use a dedicated task runner on top of pnpm) **Key patterns covered:** - `pnpm-workspace.yaml` setup and workspace package discovery - Workspace protocol (`workspace:*`, `workspace:^`, `workspace:~`) - Catalogs for dependency version synchronization - Filtering commands (`--filter`, `-F`, glob patterns, dependency selectors) - Running scripts across workspaces (`-r`, `--parallel`, `--workspace-concurrency`) - Settings in `pnpm-workspace.yaml` (v10: settings moved from `.npmrc`) - Publishing with `publishConfig` and changesets - CI/CD with GitHub Actions, caching, and `--frozen-lockfile` - Shared TypeScript configuration patterns - Monorepo directory structure conventions **Detailed resources:** - [Core Setup](examples/core.md) -- pnpm-workspace.yaml, .npmrc, directory structure, settings - [Shared Packages](examples/packages.md) -- workspace protocol, catalogs, TypeScript/ESLint config - [Scripts & Filtering](examples/scripts.md) -- --filter, recursive execution, dependency management - [Publishing & Versioning](examples/publishing.md) -- changesets, publishConfig, Docker - [CI/CD Pipelines](examples/ci.md) -- GitHub Actions, automated release - [Quick Command Reference](reference.md) -- condensed lookup table --- <philosophy> ## Philosophy pnpm workspaces provide strict, efficient monorepo management with content-addressable storage. Unlike npm/yarn, pnpm creates a non-flat `node_modules` where packages can only access their declared dependencies -- this strictness catches missing dependency declarations early. The workspace protocol (`workspace:*`) ensures internal packages always link locally, while catalogs (`catalog:`) centralize version management to eliminate version drift. **Core principles:** - **Strict by default** -- packages cannot access undeclared dependencies - **Disk efficient** -- content-addressable store shares identical files across projects - **Security first** -- v10 blocks lifecycle scripts by default to prevent supply chain attacks - **Single lockfile** -- one `pnpm-lock.yaml` at the workspace root for all packages **When to use pnpm workspaces:** - Monorepos with multiple apps and shared packages - Projects that need strict dependency isolation - Teams that want disk-efficient dependency storage - Publishing multiple related npm packages from one repo **When NOT to use:** - Single-package projects (no workspace benefits) - Projects deeply invested in Yarn PnP or Berry features - Environments where only npm is available (some CI/CD, restricted corporate setups) </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Workspace Configuration (`pnpm-workspace.yaml`) The `pnpm-workspace.yaml` file at the repository root defines which directories contain workspace packages. Every pnpm workspace MUST have this file. ```yaml # pnpm-workspace.yaml packages: - "apps/*" - "packages/*" ``` In pnpm v10, settings moved from `.npmrc` to this file: ```yaml # pnpm-workspace.yaml (v10+) packages: - "apps/*" - "packages/*" linkWorkspacePackages: true saveWorkspaceProtocol: rolling disallowWorkspaceCycles: true ``` **Why good:** Single source of truth for workspace definition and settings See [examples/core.md](examples/core.md) for full workspace setup, directory structure, and all settings. --- ### Pattern 2: Workspace Protocol (`workspace:*`) The workspace protocol ensures internal packages always resolve to the local workspace version, never from the registry. | Protocol | During Development | After `pnpm publish` | | ------------- | ---------------------- | ------------------------------------------- | | `workspace:*` | Links to local package | Replaced with exact version (e.g., `1.5.0`) | | `workspace:^` | Links to local package | Replaced with caret range (e.g., `^1.5.0`) | | `workspace:~` | Links to local package | Replaced with tilde range (e.g., `~1.5.0`) | ```json { "dependencies": { "@repo/ui": "workspace:*", "@repo/types": "workspace:*" } } ``` **Why good:** Guarantees local linking, pnpm refuses to resolve externally, version conversion on publish ensures correct semver ```json { "dependencies": { "@repo/ui": "^1.0.0" } } ``` **Why bad:** Hardcoded versions may install from npm registry instead of local workspace, version mismatches across packages See [examples/packages.md](examples/packages.md) for protocol variants, aliasing, and internal package setup. --- ### Pattern 3: Catalogs for Version Synchronization Catalogs define dependency versions once in `pnpm-workspace.yaml` and reference them across all packages with `catalog:`. ```yaml # pnpm-workspace.yaml catalog: react: ^19.0.0 react-dom: ^19.0.0 typescript: ^5.7.0 ``` ```json { "dependencies": { "react": "catalog:", "react-dom": "catalog:" } } ``` **Why good:** Single version source of truth, updating one line updates all packages, eliminates merge conflicts Named catalogs support version migration: ```yaml catalogs: react18: react: ^18.3.1 react19: react: ^19.0.0 ``` See [examples/packages.md](examples/packages.md) for named catalogs, strict enforcement, and full examples. --- ### Pattern 4: Filtering Commands `--filter` (or `-F`) restricts commands to specific packages instead of running across the entire workspace. ```bash # Exact package pnpm --filter @repo/web-app build # Package and ALL its dependencies pnpm --filter "web-app..." build # Changed packages since main pnpm --filter "...[origin/main]" test # Exclude a package pnpm --filter "!@repo/docs" build ``` **Why good:** Targeted execution saves CI time, dependency-aware filtering ensures correct build order ```bash # BAD: Running everything when only one package changed pnpm -r build ``` **Why bad:** Wastes CI time rebuilding all packages, no change detection See [examples/scripts.md](examples/scripts.md) for all filter variants, CI-optimized patterns, and graph exploration. --- ### Pattern 5: Running Scripts Across Workspaces ```bash # Topological order (respects dependency graph) -- use for build pnpm -r build # Parallel (ignores dependency graph) -- use for test, lint pnpm -r --parallel test # Controlled concurrency pnpm -r --workspace-concurrency 4 build ``` ```bash # BAD: Building in parallel when packages depend on each other pnpm -r --parallel build ``` **Why bad:** Packages may build before their dependencies finish, causing missing module failures See [examples/scripts.md](examples/scripts.md) for dependency management and adding dependencies. --- ### Pattern 6: Shared TypeScript Configuration Share TypeScript compiler options across all workspace packages using a configuration package. ```json { "name": "@repo/config-typescript", "private": true, "exports": { "./base": "./tsconfig.base.json", "./react": "./tsconfig.react.json", "./node": "./tsconfig.node.json" } } ``` Consumer usage: ```json { "extends": "@repo/config-typescript/react", "include": ["src/**/*.ts", "src/**/*.tsx"] } ``` **Why good:** Single source of truth for TypeScript settings, changes propagate to all packages See [examples/packages.md](examples/packages.md) for full base/react/node configs and ESLint sharing. --- ### Pattern 7: Publishing from Workspaces Use `publishConfig` to control what gets published and changesets for versioning. ```json { "name": "@repo/ui", "main": "./src/index.ts", "publishConfig": { "main": "./dist/index.js", "types": "./dist/index.d.ts", "access": "public" } } ``` **Why good:** Source during development (fast HMR), built output on publish ```bash pnpm changeset # Create changeset pnpm changeset version # Bump versions + changelogs pnpm publish -r # Publish to npm ``` See [examples/publishing.md](examples/publishing.md) for changesets config, Docker deployment, and migration. --- ### Pattern 8: CI/CD with GitHub Actions ```yaml - uses: pnpm/action-setup@v4 with: version: 10 - uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" - run: pnpm install - run: pnpm --filter "...[origin/main]" build - run: pnpm --filter "...[origin/main]" test ``` **Why good:** `pnpm/action-setup@v4` handles installation, `cache: "pnpm"` caches the store, `--frozen-lockfile` is automatic in CI, change-based filtering only builds affected packages See [examples/ci.md](examples/ci.md) for complete workflows including automated release with changesets. </patterns> --- <performance> ## Performance Optimization **Install Performance:** - pnpm uses content-addressable storage -- identical files are stored once on disk - Warm installs are up to 2x faster than npm/yarn due to hard linking - `--frozen-lockfile` (default in CI) skips resolution for fastest installs **Workspace Performance:** - Use `--filter` to run commands only on affected packages - Use `--parallel` for tasks without cross-package dependencies (test, lint) - Use `--workspace-concurrency` to control parallelism on resource-constrained CI - Change-based filtering (`--filter "...[origin/main]"`) skips unchanged packages **Disk Savings:** - Global store deduplicates across projects (`pnpm store path`) - Typical savings: 50-70% less disk space compared to npm **CI Caching:** ```yaml - uses: actions/setup-node@v4 with: node-version: 22 cache: "pnpm" ``` This caches the pnpm content-addressable store between runs, making subsequent installs near-instant. </performance> --- <decision_framework> ## Decision Framework ### When to Use Catalogs vs Direct Versions ``` Does 3+ packages use this dependency? YES -> Use catalog: protocol NO -> Direct version is fine Will this dependency version be updated frequently? YES -> Use catalog: (one-line update) NO -> Direct version is acceptable ``` ### workspace:\* vs workspace:^ vs workspace:~ ``` Are you publishing packages to npm? NO -> Use workspace:* (exact local linking, version irrelevant) YES -> Do consumers need flexible version ranges? YES -> workspace:^ (caret range on publish) NO -> workspace:* (exact version on publish) ``` ### When to Use --filter vs -r ``` Running a command in CI? YES -> Use --filter "...[origin/main]" (only affected packages) NO -> Running locally? ALL packages -> pnpm -r <cmd> ONE package -> pnpm --filter <name> <cmd> Is the command order-dependent (build)? YES -> Use -r (topological order) or --filter with ... NO -> Use --parallel for speed (lint, test) ``` ### pnpm vs npm vs Yarn Workspaces ``` Need strict dependency isolation? YES -> pnpm (non-flat node_modules by default) NO -> Any works Need disk efficiency? YES -> pnpm (content-addressable store) NO -> Any works Need zero-config PnP (no node_modules)? YES -> Yarn Berry with PnP NO -> pnpm or npm ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Hardcoded versions for internal packages instead of `workspace:*` (breaks local linking, installs from registry) - Settings in `.npmrc` that should be in `pnpm-workspace.yaml` (silently ignored in pnpm v10) - Missing `pnpm-workspace.yaml` at repo root (pnpm will not recognize workspace packages) - Running `pnpm install` without `--frozen-lockfile` in CI (lockfile can mutate silently) - Using `--parallel` for build commands when packages depend on each other (race conditions) **Medium Priority Issues:** - Not using `catalog:` when 3+ packages share the same dependency version (version drift) - Running `pnpm -r build` in CI instead of `--filter "...[origin/main]"` (wastes time) - Missing `private: true` on internal packages (risk of accidental npm publish) - Not configuring `allowBuilds` (or the older `onlyBuiltDependencies`) allowlist (blocks all install scripts in v10) **Common Mistakes:** - Mixing package managers (npm install in a pnpm workspace breaks the lockfile) - Using `shamefullyHoist: true` as a quick fix instead of declaring missing dependencies properly - Forgetting `fetch-depth: 0` in GitHub Actions checkout (breaks git-based change detection) - Running different pnpm versions locally vs CI (lockfile format incompatibility) **Gotchas & Edge Cases:** - `workspace:*` is replaced with the actual version on `pnpm publish` -- this is expected behavior, not a bug - `catalog:` entries must match the dependency name exactly -- typos silently fall through - `--filter "...[origin/main]"` requires git history -- use `fetch-depth: 0` or at minimum `fetch-depth: 2` - pnpm v10 blocks ALL lifecycle scripts by default -- use `pnpm approve-builds` to allowlist packages that need `postinstall` (like `esbuild`, `sharp`), or configure `allowBuilds` in `pnpm-workspace.yaml` - `injectWorkspacePackages: true` is required for `pnpm deploy` (or use `pnpm deploy --legacy` to bypass) - Circular workspace dependencies cause unpredictable script execution order -- use `disallowWorkspaceCycles: true` - `saveWorkspaceProtocol: rolling` (default) means `pnpm add` auto-saves with `workspace:` -- this is correct behavior </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use `workspace:*` protocol for ALL internal package dependencies -- never hardcode versions)** **(You MUST use `--frozen-lockfile` in CI -- pnpm enables this by default in CI environments)** **(You MUST define workspace packages in `pnpm-workspace.yaml` at the repository root)** **(You MUST put pnpm-specific settings in `pnpm-workspace.yaml` -- NOT `.npmrc` (pnpm v10 change))** **(You MUST use `catalog:` protocol when sharing dependency versions across 3+ packages)** **(You MUST use `--filter` for targeted commands instead of `pnpm -r` when only specific packages changed)** **Failure to follow these rules will cause broken dependency resolution, version drift, missed CI caching, and security vulnerabilities.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.