.agents/skills/turborepo/references/best-practices/packages.md
How to create and structure internal packages in your monorepo.
packages/package.json with name and exportssrc/tsconfig.json if using TypeScriptExport 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:
Limitations:
paths (use Node.js subpath imports instead)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:
Remember: Add dist/** to turbo.json outputs!
{
"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
}
}
{
"exports": {
"./button": {
"types": "./src/button.tsx",
"import": "./dist/button.mjs",
"require": "./dist/button.cjs",
"default": "./dist/button.js"
}
}
}
// apps/web/package.json
{
"dependencies": {
"@repo/ui": "workspace:*" // pnpm/bun
// "@repo/ui": "*" // npm/yarn
}
}
pnpm install # Updates lockfile with new dependency
// apps/web/src/page.tsx
import { Button } from '@repo/ui/button';
export default function Page() {
return <Button>Click me</Button>;
}
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
// 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
// packages/typescript-config/package.json
{
"name": "@repo/typescript-config",
"exports": {
"./base.json": "./base.json",
"./nextjs.json": "./nextjs.json",
"./library.json": "./library.json"
}
}
// 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"
}
}
// BAD: No exports defined
{
"name": "@repo/ui"
}
// GOOD: Clear exports
{
"name": "@repo/ui",
"exports": {
"./button": "./src/button.tsx"
}
}
// pnpm/bun
{ "@repo/ui": "workspace:*" } // Correct
// npm/yarn
{ "@repo/ui": "*" } // Correct
{ "@repo/ui": "workspace:*" } // Wrong for npm/yarn!
// Package builds to dist/, but turbo.json doesn't know
{
"tasks": {
"build": {
"outputs": [".next/**"] // Missing dist/**!
}
}
}
// Correct
{
"tasks": {
"build": {
"outputs": [".next/**", "dist/**"]
}
}
}
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
tsc for Internal PackagesFor internal packages, prefer tsc over bundlers. Bundlers can mangle code before it reaches your app's bundler, causing hard-to-debug issues.
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.
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.
They add complexity and another caching layer. Turborepo handles dependencies better.