AgentSkillsCN

monorepo-management

为基于 pnpm 的单体仓库管理提供模式:工作区边界、依赖策略、构建缓存、共享工具链,以及协调一致的发布流程。

SKILL.md
--- frontmatter
name: monorepo-management
description: Patterns for managing pnpm-based monorepos: workspace boundaries, dependency strategy, build caching, shared tooling, and coordinated releases.
argument-hint: Describe your monorepo issue (workspace layout, dependency constraints, build caching, shared configs, versioning, releases).

Monorepo Management

Overview

A monorepo is a trade:

  • ✅ Shared tooling and unified refactors
  • ✅ Code reuse across apps/packages
  • ✅ Single source of truth for types and utilities
  • ❌ Larger dependency graphs (harder to reason about)
  • ❌ Build and CI complexity if not designed intentionally

Buzz Stack is a single Next.js app repo, but many teams evolve it into a monorepo when they add:

  • a shared design system
  • shared API contracts / types
  • background workers or separate deployables
  • multiple Next.js apps

This skill focuses on a pragmatic “pnpm-first” approach.

Core Concepts

1) Workspace Organization: Packages, Apps, and Boundaries

A common structure:

code
.
├─ apps/
│  ├─ web/            # Next.js app (Buzz Stack)
│  └─ admin/          # optional second app
├─ packages/
│  ├─ ui/             # design system components
│  ├─ types/          # shared TypeScript types
│  ├─ config/         # shared eslint/tsconfig/tailwind presets
│  └─ utils/          # pure utilities
└─ pnpm-workspace.yaml

Principles:

  • Keep “deployables” (apps/services) in apps/
  • Keep “libraries” in packages/
  • Enforce boundaries so apps depend on packages, not the other way around

2) pnpm Workspaces: The Contract

pnpm-workspace.yaml declares package locations.

yaml
packages:
  - "apps/*"
  - "packages/*"

Why pnpm matters:

  • pnpm’s store + symlink model discourages undeclared dependencies
  • workspace protocol (workspace:*) makes internal deps explicit

3) Dependency Management Strategy

Your goal is to keep the dependency graph:

  • shallow
  • acyclic
  • explicit

Key tools:

  • workspace:* protocol
  • peer dependencies for host-controlled libs (React, Next)
  • constraints/lint rules to enforce boundaries

Example: internal dependency via workspace protocol.

json
{
  "name": "@acme/web",
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@acme/types": "workspace:*"
  }
}

Peer deps example for UI package:

json
{
  "name": "@acme/ui",
  "peerDependencies": {
    "react": "^19",
    "react-dom": "^19"
  }
}

4) Shared Tooling: ESLint, TypeScript, Tailwind

Monorepos should centralize configuration, then consume it.

TypeScript pattern:

  • root tsconfig.base.json
  • each package has tsconfig.json that extends base
json
// tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "noEmit": true
  }
}
json
// packages/ui/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "jsx": "react-jsx"
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"]
}

ESLint flat config:

  • export a shared flat config from packages/config/eslint/
  • each package imports and extends it

Buzz Stack already uses ESLint 9 flat config at the repo root (see eslint.config.mjs).


5) Build Strategies: Incremental, Cached, and “Affected”

Monorepo builds break down if everything rebuilds on every change.

Common approaches:

  • TurboRepo pipelines (turbo.json)
  • Nx affected builds
  • custom scripts using git diff

Turbo example:

json
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "lint": {
      "outputs": []
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}

6) Release and Versioning Coordination

You have two primary strategies:

  1. Single version (locked-step):

    • simpler for internal-only repos
    • one release tag
  2. Independent versions:

    • necessary if you publish packages separately
    • requires tooling (changesets, semantic-release)

Pattern: use conventional commits consistently.

text
feat(ui): add accessible dropdown
fix(web): avoid double fetch in server component
chore(config): align eslint rules

Patterns (10+)

Pattern 1: Minimal pnpm Workspace Setup

yaml
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
json
// package.json (root)
{
  "private": true,
  "packageManager": "pnpm@9.0.0",
  "scripts": {
    "lint": "pnpm -r lint",
    "build": "pnpm -r build",
    "test": "pnpm -r test"
  }
}

Pattern 2: Package Boundary Rules (No Deep Imports)

Enforce that consumers import from package entrypoints.

typescript
// ✅ good
import { Button } from "@acme/ui";

// ❌ bad
import { Button } from "@acme/ui/src/components/Button";

Why:

  • deep imports bypass semver
  • break builds when file layout changes

Pattern 3: Shared @acme/types Package

Keep shared DTOs and domain types in a dedicated package.

typescript
// packages/types/src/user.ts
export interface UserDto {
  id: string;
  email: string;
  name: string;
}

Apps and services depend on the same types.


Pattern 4: Shared ESlint Flat Config Package

javascript
// packages/config/eslint/index.mjs
import js from "@eslint/js";

export const baseConfig = [js.configs.recommended];

Each package:

javascript
// apps/web/eslint.config.mjs
import { baseConfig } from "@acme/config-eslint";

export default [...baseConfig];

Pattern 5: Shared Tailwind Preset Package

typescript
// packages/config/tailwind/preset.ts
import type { Config } from "tailwindcss";

const preset: Partial<Config> = {
  theme: {
    extend: {},
  },
};

export default preset;

Consume:

typescript
// apps/web/tailwind.config.ts
import type { Config } from "tailwindcss";
import preset from "@acme/config-tailwind";

const config: Config = {
  presets: [preset],
  content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
};

export default config;

Pattern 6: Workspace Protocol Everywhere for Internal Packages

json
{
  "dependencies": {
    "@acme/utils": "workspace:*"
  }
}

Why:

  • prevents accidental publishing constraints
  • ensures local dev uses the local package

Pattern 7: Peer Dependencies for React/Next in Shared UI Packages

json
{
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

Pattern 8: Build Outputs and exports for Libraries

json
{
  "name": "@acme/utils",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

Pattern 9: Affected Builds in CI (Turbo/Nx)

Pseudo pipeline:

  • compute changed packages
  • build only impacted apps
text
changed: packages/types
affected: packages/ui, apps/web

Pattern 10: Keep Server-Only and Client-Only Code Separate

In Next.js monorepos, split packages by runtime:

  • @acme/server-* packages can use Node APIs
  • @acme/client-* packages must be browser-safe

Pattern 11: Consistent TypeScript Settings Across Packages

json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

Avoid “weak” packages that leak unsoundness.


Pattern 12: Tooling Scripts as a Package

Buzz Stack already uses orchestration scripts (see scripts/subagent-toolkit.mjs). In monorepos, centralize similar tooling in packages/tooling/.


Anti-Patterns

Anti-pattern 1: Cyclic Dependencies Between Packages

If ui -> types -> ui, you will suffer.

Fix:

  • extract shared primitives into a third package
  • enforce dependency direction rules

Anti-pattern 2: Hidden Dependencies via Hoisting Assumptions

If code works only because Node resolves a transitive dependency, it will break.

Fix:

  • declare deps explicitly
  • rely on pnpm strictness to catch missing deps

Anti-pattern 3: Publishing Internal Packages Without Build/Exports Discipline

Fix:

  • produce stable dist/
  • define exports
  • test consumer imports

Anti-pattern 4: Multiple TS/ESLint Config Dialects Across Packages

Fix:

  • centralize config packages
  • extend them everywhere

Anti-pattern 5: Shared Package That Imports App Code

Fix:

  • keep shared packages app-agnostic
  • pass dependencies in via interfaces

Real-World Buzz Stack Anchors

Even in a single-repo setup, Buzz Stack demonstrates monorepo-adjacent patterns:

  • pnpm is the declared package manager (package.json)
  • ESLint 9 flat config is centralized (eslint.config.mjs)
  • orchestration tooling lives in scripts/ (useful precursor to packages/tooling)

When converting Buzz Stack to a monorepo, keep these ideas:

  • one linting dialect
  • strict TS everywhere
  • explicit package boundaries

Cross-References

  • Skills:

    • buzz-stack-dev-workflow: ../buzz-stack-dev-workflow/SKILL.md
    • eslint-flat-config-workflow: ../eslint-flat-config-workflow/SKILL.md
    • architecture-design: ../architecture-design/SKILL.md
    • performance-profiling: ../performance-profiling/SKILL.md
  • Docs:

    • Code structure: ../../../docs/CODE_STRUCTURE.md
    • Development workflow: ../../../docs/DEVELOPMENT.md

Quick Checklist

  • Is pnpm-workspace.yaml explicit and minimal?
  • Are internal deps workspace:*?
  • Do packages have clear runtime constraints?
  • Is build cached and incremental?
  • Are releases predictable (single vs independent versioning decided)?