Files
website-enchun-mgr/.opencode/skills/turborepo/references/best-practices/structure.md
pkupuk ad8e2e313e chore(agent): configure AI agents and tools
Add configuration for BMad, Claude, OpenCode, and other AI agent tools and workflows.
2026-02-11 11:51:23 +08:00

4.9 KiB

Repository Structure

Detailed guidance on structuring a Turborepo monorepo.

Workspace Configuration

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

npm/yarn/bun

// package.json
{
  "workspaces": ["apps/*", "packages/*"]
}

Root package.json

{
  "name": "my-monorepo",
  "private": true,
  "packageManager": "pnpm@9.0.0",
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "latest"
  }
}

Key points:

  • private: true - Prevents accidental publishing
  • packageManager - Enforces consistent package manager version
  • Scripts only delegate to turbo run - No actual build logic here!
  • Minimal devDependencies (just turbo and repo tools)

Always Prefer Package Tasks

Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.

// packages/web/package.json
{
  "scripts": {
    "build": "next build",
    "lint": "eslint .",
    "test": "vitest",
    "typecheck": "tsc --noEmit"
  }
}

// packages/api/package.json
{
  "scripts": {
    "build": "tsc",
    "lint": "eslint .",
    "test": "vitest",
    "typecheck": "tsc --noEmit"
  }
}

Package tasks enable Turborepo to:

  1. Parallelize - Run web#lint and api#lint simultaneously
  2. Cache individually - Each package's task output is cached separately
  3. Filter precisely - Run turbo run test --filter=web for just one package

Root Tasks are a fallback for tasks that truly cannot run per-package:

// AVOID unless necessary - sequential, not parallelized, can't filter
{
  "scripts": {
    "lint": "eslint apps/web && eslint apps/api && eslint packages/ui"
  }
}

Root turbo.json

{
  "$schema": "https://turborepo.dev/schema.v2.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {},
    "test": {
      "dependsOn": ["build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Directory Organization

Grouping Packages

You can group packages by adding more workspace paths:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
  - "packages/config/*"    # Grouped configs
  - "packages/features/*"  # Feature packages

This allows:

packages/
├── ui/
├── utils/
├── config/
│   ├── eslint/
│   ├── typescript/
│   └── tailwind/
└── features/
    ├── auth/
    └── payments/

What NOT to Do

# BAD: Nested wildcards cause ambiguous behavior
packages:
  - "packages/**"  # Don't do this!

Package Anatomy

Minimum Required Files

packages/ui/
├── package.json    # Required: Makes it a package
├── src/            # Source code
│   └── button.tsx
└── tsconfig.json   # TypeScript config (if using TS)

package.json Requirements

{
  "name": "@repo/ui",           // Unique, namespaced name
  "version": "0.0.0",           // Version (can be 0.0.0 for internal)
  "private": true,              // Prevents accidental publishing
  "exports": {                  // Entry points
    "./button": "./src/button.tsx"
  }
}

TypeScript Configuration

Shared Base Config

Create a shared TypeScript config package:

packages/
└── typescript-config/
    ├── package.json
    ├── base.json
    ├── nextjs.json
    └── library.json
// packages/typescript-config/base.json
{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "module": "ESNext",
    "target": "ES2022"
  }
}

Extending in Packages

// packages/ui/tsconfig.json
{
  "extends": "@repo/typescript-config/library.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

No Root tsconfig.json

You likely don't need a tsconfig.json in the workspace root. Each package should have its own config extending from the shared config package.

ESLint Configuration

Shared Config Package

packages/
└── eslint-config/
    ├── package.json
    ├── base.js
    ├── next.js
    └── library.js
// packages/eslint-config/package.json
{
  "name": "@repo/eslint-config",
  "exports": {
    "./base": "./base.js",
    "./next": "./next.js",
    "./library": "./library.js"
  }
}

Using in Packages

// apps/web/.eslintrc.js
module.exports = {
  extends: ["@repo/eslint-config/next"],
};

Lockfile

A lockfile is required for:

  • Reproducible builds
  • Turborepo to understand package dependencies
  • Cache correctness

Without a lockfile, you'll see unpredictable behavior.