PNPM Workspaces (Constructive Standard)
Create and manage PNPM monorepo workspaces following Constructive conventions. This covers pure TypeScript/JavaScript workspaces (not pgpm workspaces for SQL modules).
When to Apply
Use this skill when:
-
Creating a new TypeScript/JavaScript monorepo
-
Setting up a pnpm workspace structure
-
Configuring lerna for versioning and publishing
-
Managing internal package dependencies
Workspace Structure
A Constructive-standard pnpm workspace:
my-workspace/ ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── lerna.json ├── package.json ├── packages/ │ ├── package-a/ │ │ ├── package.json │ │ ├── src/ │ │ └── tsconfig.json │ └── package-b/ │ ├── package.json │ ├── src/ │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json
Core Configuration Files
pnpm-workspace.yaml
Defines which directories contain packages:
packages:
- 'packages/*'
For larger projects with multiple package directories:
packages:
- 'packages/*'
- 'apps/*'
- 'libs/*'
Root package.json
{ "name": "my-workspace", "version": "0.0.1", "private": true, "repository": { "type": "git", "url": "https://github.com/org/my-workspace" }, "license": "MIT", "publishConfig": { "access": "restricted" }, "scripts": { "build": "pnpm -r run build", "clean": "pnpm -r run clean", "test": "pnpm -r run test", "lint": "pnpm -r run lint", "deps": "pnpm up -r -i -L" }, "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^22.10.2", "@typescript-eslint/eslint-plugin": "^8.53.1", "@typescript-eslint/parser": "^8.53.1", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "jest": "^30.2.0", "lerna": "^8.2.4", "prettier": "^3.8.0", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", "typescript": "^5.6.3" } }
Key points:
-
Root package is private: true (never published)
-
Scripts use pnpm -r to run recursively across packages
-
deps script for interactive dependency updates
lerna.json
{ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "version": "independent", "npmClient": "pnpm", "registry": "https://registry.npmjs.org", "command": { "create": { "homepage": "https://github.com/org/my-workspace", "license": "MIT", "access": "restricted" }, "publish": { "allowBranch": "main", "message": "chore(release): publish", "conventionalCommits": true } } }
Versioning modes:
-
"version": "independent" — Each package versioned separately (recommended for utility libraries)
-
"version": "0.0.1" — Fixed versioning, all packages share same version (recommended for tightly coupled packages)
Package Configuration
Individual package.json
{ "name": "my-package", "version": "0.1.0", "description": "Package description", "author": "Constructive <developers@constructive.io>", "main": "index.js", "module": "esm/index.js", "types": "index.d.ts", "homepage": "https://github.com/org/my-workspace", "license": "MIT", "publishConfig": { "access": "public", "directory": "dist" }, "repository": { "type": "git", "url": "https://github.com/org/my-workspace" }, "scripts": { "clean": "makage clean", "build": "makage build", "lint": "eslint . --fix", "test": "jest" }, "devDependencies": { "makage": "0.1.10" } }
Key patterns:
-
publishConfig.directory: "dist" — Publish from dist folder (prevents tree-shaking into weird paths)
-
main and module point to built files (not src)
-
Uses makage for build tooling
Internal Dependencies
Reference workspace packages using workspace:* :
{ "dependencies": { "my-other-package": "workspace:*" } }
When published, workspace:* is replaced with the actual version number.
Common Commands
Command Description
pnpm install
Install all dependencies
pnpm -r run build
Build all packages
pnpm -r run test
Test all packages
pnpm --filter <pkg> run build
Build specific package
pnpm up -r -i -L
Interactive dependency update
pnpm lerna version
Version packages
pnpm lerna publish
Publish packages
Creating a New Package
Create package directory
mkdir -p packages/my-new-package/src
Initialize package.json
cd packages/my-new-package pnpm init
Then configure package.json following the pattern above.
TypeScript Configuration
Root tsconfig.json
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "declaration": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true } }
Package tsconfig.json
{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"] }
PNPM vs PGPM Workspaces
Aspect PNPM Workspace PGPM Workspace
Purpose TypeScript/JS packages SQL database modules
Config pnpm-workspace.yaml pnpm-workspace.yaml + pgpm.json
Build makage build pgpm package
Output dist/ folder SQL bundles
Registry npm npm (for @pgpm/* packages)
Some repos (like constructive) are hybrid — they have both pnpm packages and pgpm modules.
Best Practices
-
Keep root private: Never publish the root package
-
Use workspace protocol: Always use workspace:* for internal deps
-
Consistent structure: All packages follow same directory layout
-
Shared config: Extend root tsconfig.json in packages
-
Independent versioning: Use for utility libraries with different release cycles
-
Fixed versioning: Use for tightly coupled packages that should release together
References
-
Related skill: pnpm-publishing for publishing workflow with makage
-
Related skill: pgpm-workspace for SQL module workspaces
-
Related skill: pgpm-publishing for publishing pgpm modules