Add configuration for BMad, Claude, OpenCode, and other AI agent tools and workflows.
6.2 KiB
Creating Internal Packages
How to create and structure internal packages in your monorepo.
Package Creation Checklist
- Create directory in
packages/ - Add
package.jsonwith name and exports - Add source code in
src/ - Add
tsconfig.jsonif using TypeScript - Install as dependency in consuming packages
- 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.