Files
website-enchun-mgr/.opencode/skills/turborepo/references/best-practices/packages.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

6.2 KiB

Creating Internal Packages

How to create and structure internal packages in your monorepo.

Package Creation Checklist

  1. Create directory in packages/
  2. Add package.json with name and exports
  3. Add source code in src/
  4. Add tsconfig.json if using TypeScript
  5. Install as dependency in consuming packages
  6. Run package manager install to update lockfile

Package Compilation Strategies

Just-in-Time (JIT)

Export TypeScript directly. The consuming app's bundler compiles it.

// packages/ui/package.json
{
  "name": "@repo/ui",
  "exports": {
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx"
  },
  "scripts": {
    "lint": "eslint .",
    "check-types": "tsc --noEmit"
  }
}

When to use:

  • Apps use modern bundlers (Turbopack, webpack, Vite)
  • You want minimal configuration
  • Build times are acceptable without caching

Limitations:

  • No Turborepo cache for the package itself
  • Consumer must support TypeScript compilation
  • Can't use TypeScript paths (use Node.js subpath imports instead)

Compiled

Package handles its own compilation.

// packages/ui/package.json
{
  "name": "@repo/ui",
  "exports": {
    "./button": {
      "types": "./src/button.tsx",
      "default": "./dist/button.js"
    }
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  }
}
// packages/ui/tsconfig.json
{
  "extends": "@repo/typescript-config/library.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

When to use:

  • You want Turborepo to cache builds
  • Package will be used by non-bundler tools
  • You need maximum compatibility

Remember: Add dist/** to turbo.json outputs!

Defining Exports

Multiple Entrypoints

{
  "exports": {
    ".": "./src/index.ts",           // @repo/ui
    "./button": "./src/button.tsx",  // @repo/ui/button
    "./card": "./src/card.tsx",      // @repo/ui/card
    "./hooks": "./src/hooks/index.ts" // @repo/ui/hooks
  }
}

Conditional Exports (Compiled)

{
  "exports": {
    "./button": {
      "types": "./src/button.tsx",
      "import": "./dist/button.mjs",
      "require": "./dist/button.cjs",
      "default": "./dist/button.js"
    }
  }
}

Installing Internal Packages

Add to Consuming Package

// apps/web/package.json
{
  "dependencies": {
    "@repo/ui": "workspace:*"  // pnpm/bun
    // "@repo/ui": "*"         // npm/yarn
  }
}

Run Install

pnpm install  # Updates lockfile with new dependency

Import and Use

// apps/web/src/page.tsx
import { Button } from '@repo/ui/button';

export default function Page() {
  return <Button>Click me</Button>;
}

One Purpose Per Package

Good Examples

packages/
├── ui/                  # Shared UI components
├── utils/               # General utilities
├── auth/                # Authentication logic
├── database/            # Database client/schemas
├── eslint-config/       # ESLint configuration
├── typescript-config/   # TypeScript configuration
└── api-client/          # Generated API client

Avoid Mega-Packages

// BAD: One package for everything
packages/
└── shared/
    ├── components/
    ├── utils/
    ├── hooks/
    ├── types/
    └── api/

// GOOD: Separate by purpose
packages/
├── ui/          # Components
├── utils/       # Utilities
├── hooks/       # React hooks
├── types/       # Shared TypeScript types
└── api-client/  # API utilities

Config Packages

TypeScript Config

// packages/typescript-config/package.json
{
  "name": "@repo/typescript-config",
  "exports": {
    "./base.json": "./base.json",
    "./nextjs.json": "./nextjs.json",
    "./library.json": "./library.json"
  }
}

ESLint Config

// packages/eslint-config/package.json
{
  "name": "@repo/eslint-config",
  "exports": {
    "./base": "./base.js",
    "./next": "./next.js"
  },
  "dependencies": {
    "eslint": "^8.0.0",
    "eslint-config-next": "latest"
  }
}

Common Mistakes

Forgetting to Export

// BAD: No exports defined
{
  "name": "@repo/ui"
}

// GOOD: Clear exports
{
  "name": "@repo/ui",
  "exports": {
    "./button": "./src/button.tsx"
  }
}

Wrong Workspace Syntax

// pnpm/bun
{ "@repo/ui": "workspace:*" }  // Correct

// npm/yarn
{ "@repo/ui": "*" }            // Correct
{ "@repo/ui": "workspace:*" }  // Wrong for npm/yarn!

Missing from turbo.json Outputs

// Package builds to dist/, but turbo.json doesn't know
{
  "tasks": {
    "build": {
      "outputs": [".next/**"]  // Missing dist/**!
    }
  }
}

// Correct
{
  "tasks": {
    "build": {
      "outputs": [".next/**", "dist/**"]
    }
  }
}

TypeScript Best Practices

Use Node.js Subpath Imports (Not paths)

TypeScript compilerOptions.paths breaks with JIT packages. Use Node.js subpath imports instead (TypeScript 5.4+).

JIT Package:

// packages/ui/package.json
{
  "imports": {
    "#*": "./src/*"
  }
}
// packages/ui/button.tsx
import { MY_STRING } from "#utils.ts";  // Uses .ts extension

Compiled Package:

// packages/ui/package.json
{
  "imports": {
    "#*": "./dist/*"
  }
}
// packages/ui/button.tsx
import { MY_STRING } from "#utils.js";  // Uses .js extension

Use tsc for Internal Packages

For internal packages, prefer tsc over bundlers. Bundlers can mangle code before it reaches your app's bundler, causing hard-to-debug issues.

Enable Go-to-Definition

For Compiled Packages, enable declaration maps:

// tsconfig.json
{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true
  }
}

This creates .d.ts and .d.ts.map files for IDE navigation.

No Root tsconfig.json Needed

Each package should have its own tsconfig.json. A root one causes all tasks to miss cache when changed. Only use root tsconfig.json for non-package scripts.

Avoid TypeScript Project References

They add complexity and another caching layer. Turborepo handles dependencies better.