monorepo-guidelines
pnpm workspace monorepo patterns — workspace setup, package anatomy, TypeScript project references, build ordering, versioning, and publishing
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
pnpm workspace monorepo patterns — workspace setup, package anatomy, TypeScript project references, build ordering, versioning, and publishing
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Security guidelines for TypeScript and Node.js — input validation, injection prevention, authentication hardening, secrets management, HTTP security headers, and common vulnerability patterns
In-memory caching patterns for TypeScript — memoization, async deduplication, TTL, LRU, stale-while-revalidate, and invalidation strategies
Concrete patterns for composing behavior from small units — function composition, middleware, decorators, mixins, monad chaining, builders, plugin systems, strategy composition, and type-level composition for TypeScript
ESM-specific patterns for TypeScript and Node.js — file extensions, import.meta, dynamic imports, dual packages, and module resolution modes
Guidelines for designing and implementing HTTP REST APIs — resource naming, HTTP semantics, status codes, error responses, pagination, and versioning
Production-ready Playwright patterns — config, locators, assertions, page objects, network mocking, and CI setup for reliable E2E tests.
| name | monorepo-guidelines |
| description | pnpm workspace monorepo patterns — workspace setup, package anatomy, TypeScript project references, build ordering, versioning, and publishing |
Practical patterns for managing a pnpm workspace monorepo. Covers structure, build pipeline, TypeScript project references, and publishing.
pnpm-workspace.yamlDeclare which directories are packages:
packages:
- "packages/*"
- "apps/*"
Root package.json holds only dev tooling and workspace-wide scripts. Never put runtime dependencies at the root.
{
"private": true,
"scripts": {
"build": "pnpm -r build",
"test": "pnpm -r test",
"lint": "pnpm -r lint"
},
"devDependencies": {
"typescript": "^5.5.0",
"prettier": "^3.3.0"
}
}
Each package is self-contained:
packages/core/
package.json # own name, version, deps
tsconfig.json # extends root, adds composite
src/
index.ts
tests/
index.test.ts
package.json minimum shape:
{
"name": "@scope/core",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "node --test"
}
}
No barrel re-exports. Consumers import from subpaths directly:
// ✅ — import from the package's declared export subpath
import { parse } from "@scope/core/parse";
// ❌ — barrel re-export: index.ts re-exports everything, defeating tree-shaking
import { parse } from "@scope/core"; // only acceptable if this is the canonical single entry
workspace: ProtocolReference sibling packages without publishing:
{
"dependencies": {
"@scope/core": "workspace:*"
}
}
workspace:* resolves to the local package during development. On publish, pnpm publish replaces it with the actual resolved semver version from the package's package.json. This means:
workspace:* leaksUse workspace:^ or workspace:~ if you need a range on publish instead of exact.
pnpm -r build runs build scripts in all packages in topological order — packages with no dependencies first.
Common filter patterns:
# Build only a single package and its dependencies
pnpm --filter @scope/app build
# Build packages that depend on @scope/core (affected)
pnpm --filter ...'@scope/core' build
# Build everything except one package
pnpm --filter '!@scope/docs' -r build
Always define the build script in every package — pnpm -r skips packages that lack the script.
Project references give TypeScript cross-package type-checking and incremental builds without running tsc on the whole tree.
tsconfig.jsonThe root config is the base — it defines shared compiler options but is not a project itself:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist"
}
}
tsconfig.jsonEach package extends root and enables composite:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./dist"
},
"references": [{ "path": "../core" }],
"include": ["src"]
}
composite: true requirements:
rootDir must be setdeclaration: true is impliedrootDirBuild the whole graph from the root with:
tsc --build packages/app
This walks references and rebuilds only what changed.
Place shared config at the root and extend per-package:
.prettierrc — one file at root, no per-package overrides.
.gitignore — root covers node_modules/, dist/, .tsbuildinfo. Packages add their own only for package-specific artifacts.
tsconfig.json — root defines compilerOptions; packages extend with composite and references. Never duplicate compiler options across packages.
| Script | Where to run | Why |
|---|---|---|
build | pnpm -r build | Topological — respects dependency order |
test | pnpm -r test | Independent per package, safe to parallelise |
lint | Root or -r | Shared config — root invocation is fine |
type-check | tsc --build | Project references handle the graph |
prepublishOnly | Per-package | Must run in package context before publish |
Avoid running -r for scripts that must run in a specific order — use --filter chains or tsc --build instead.
Two strategies — pick one and stay consistent:
Lockstep (fixed): All packages share the same version. Simpler, but every package bumps even if unchanged. Best for tightly-coupled packages released together.
Independent: Each package has its own version. More accurate, but consumers must track separate changelogs.
Changesets is the standard tool for both strategies:
# Record a change
pnpm changeset
# Bump versions based on recorded changesets
pnpm changeset version
# Publish all changed packages
pnpm changeset publish
Commit changeset files (.changeset/*.md) alongside the code changes that caused them. Never manually edit CHANGELOG.md.
package.json publishing fields{
"name": "@scope/core",
"version": "1.2.0",
"type": "module",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"prepublishOnly": "pnpm build"
}
}
Key rules:
files whitelist — only ship dist/, never src/ or tests/exports map — required for subpath imports and dual CJS/ESMpublishConfig.access: "public" — required for scoped packages on the public registryprepublishOnly — always rebuild before publish; never publish stale dist/pnpm publish --dry-run
Verify the files field ships exactly what you intend — no source leaks, no missing types.
| Mistake | Fix |
|---|---|
Runtime dep in root package.json | Move it to the package that needs it |
workspace:* left in published tarball | Use pnpm publish — it rewrites workspace: to resolved semver |
Missing composite: true in per-package tsconfig | Project references require composite on every referenced package |
tsc -p tsconfig.json instead of tsc --build for multi-package builds | tsc --build walks references; -p compiles one package only |
No prepublishOnly script | Stale dist/ gets published; always rebuild before publish |
Barrel re-exports in src/index.ts | Import directly from source modules; barrels cause circular deps |
pnpm -r build ignores a package | Package is missing a build script — -r skips packages without it |
exports map missing types condition | Consumers get no type-checking; always pair each entry with types |
Manually editing CHANGELOG.md | Use changesets — manual edits break the automated release flow |
outDir not set per-package | TypeScript emits into source tree; always set outDir: "./dist" |