Creating Packages
Build reusable, type-safe packages that work across your entire monorepo — from React components to utilities, with proper TypeScript config, ESLint, testing, and versioning.
Know what you're building?
Why creating packages matters
Building packages without proper structure leads to:
- Import chaos — Apps can't find exports, circular dependencies break builds
- Version conflicts — Wrong peer dependencies cause React version mismatches
- TypeScript errors — Missing tsconfig paths, type exports not found
- Publishing nightmares — Changesets fail, versions out of sync
- Testing gaps — Can't test packages in isolation, hard to mock
- Configuration drift — Each package uses different ESLint rules, formatting
Creating packages with proper setup — Correct package.json with workspace:* protocol, TypeScript config extending shared bases, ESLint configuration, clear exports, peer dependencies for React packages, Vitest configuration, and changesets integration — ensures packages work reliably across all apps.
Production-ready with 35+ existing packages (UI components, authentication, database clients, AI integration), automated dependency management via workspace protocol, shared ESLint/TypeScript configs for consistency, comprehensive testing with Vitest, and versioning via Changesets.
Use cases
Create packages to:
- Share UI components — Build design system components used across all apps
- Extract utilities — Create reusable functions for validation, formatting, date handling
- Isolate features — Build authentication, database clients, API wrappers as packages
- Team collaboration — Share code between team workspaces with clear boundaries
- Version independently — Publish changes without rebuilding entire monorepo
- Test in isolation — Write comprehensive tests for packages separately from apps
Quick Start
Essential steps
Create a shared package in 3 steps:
1. Create package structure
# Create directory and files
mkdir -p packages/my-package/src
# Create package.json with proper configuration2. Configure TypeScript, ESLint, and exports
// packages/my-package/package.json
{
"name": "@repo/my-package",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/config": "workspace:*",
"typescript": "^5.0.0"
}
}3. Create entry point and use in apps
// packages/my-package/src/index.ts
export { myFunction } from "./myFunction";
export type { MyType } from "./types";# Add to an app
pnpm --filter=web add @repo/my-packageThat's it! You now have a working package. Apps can import from @repo/my-package.
Package structure
Standard layout
Every package follows this structure:
packages/my-package/
├── package.json # Name, version, dependencies, scripts
├── tsconfig.json # Extends shared TypeScript config
├── eslint.config.mjs # Extends shared ESLint config
├── vitest.config.ts # Test configuration (optional)
├── src/
│ ├── index.ts # Public exports (entry point)
│ ├── MyComponent.tsx # Implementation
│ ├── utils.ts # Internal utilities (not exported)
│ └── types.ts # Type definitions
└── README.md # Documentation (optional)Complete package.json example
{
"name": "@repo/my-package",
"version": "0.0.0",
"private": true,
"description": "Shared utilities for the OneApp monorepo",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./utils": "./src/utils.ts"
},
"scripts": {
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"zod": "^4.0.0"
},
"devDependencies": {
"@repo/config": "workspace:*",
"@repo/types": "workspace:*",
"typescript": "^5.0.0",
"vitest": "^4.0.0"
},
"peerDependencies": {},
"keywords": ["monorepo", "utilities"],
"author": "Your Team",
"license": "MIT"
}Key fields:
- name — Must start with
@repo/for shared packages,@team-name/for team packages - version — Start at
0.0.0, Changesets will manage versions - private — Set to
truefor internal packages (not published to npm) - main — Entry point for Node.js (usually
./src/index.ts) - types — TypeScript type definitions (same as main for source packages)
- exports — Modern way to define package entry points
- scripts — Include lint, typecheck, test
- devDependencies — Use
workspace:*for internal packages - peerDependencies — For React packages (see React component packages)
Package types
React component package
For UI components that use React:
{
"name": "@repo/my-ui",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./Button": "./src/Button.tsx",
"./Card": "./src/Card.tsx"
},
"scripts": {
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"clsx": "^2.1.0"
},
"devDependencies": {
"@repo/config": "workspace:*",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}tsconfig.json:
{
"extends": "@repo/config/typescript/react.json",
"compilerOptions": {
"outDir": "dist",
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.tsx"]
}eslint.config.mjs:
import reactConfig from "@repo/config/eslint/react";
export default [...reactConfig];Example component:
// src/Button.tsx
import type { ReactNode } from "react";
import { clsx } from "clsx";
export interface ButtonProps {
children: ReactNode;
variant?: "primary" | "secondary" | "outline";
size?: "sm" | "md" | "lg";
onClick?: () => void;
disabled?: boolean;
}
/**
* A reusable button component.
*
* @example
* ```tsx
* <Button variant="primary" size="md">
* Click me
* </Button>
* ```
*/
export function Button({
children,
variant = "primary",
size = "md",
onClick,
disabled = false,
}: ButtonProps): JSX.Element {
return (
<button
onClick={onClick}
disabled={disabled}
className={clsx(
"rounded font-medium transition",
{
"bg-blue-600 text-white hover:bg-blue-700": variant === "primary",
"bg-gray-200 text-gray-900 hover:bg-gray-300": variant === "secondary",
"border-2 border-blue-600 text-blue-600 hover:bg-blue-50": variant === "outline",
},
{
"px-3 py-1.5 text-sm": size === "sm",
"px-4 py-2 text-base": size === "md",
"px-6 py-3 text-lg": size === "lg",
},
{ "opacity-50 cursor-not-allowed": disabled }
)}
>
{children}
</button>
);
}Export from index:
// src/index.ts
export { Button } from "./Button";
export type { ButtonProps } from "./Button";
export { Card } from "./Card";
export type { CardProps } from "./Card";That's it! Your UI component package is ready to use.
Utility package
For shared functions (no React dependency):
{
"name": "@repo/my-utils",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"zod": "^4.0.0"
},
"devDependencies": {
"@repo/config": "workspace:*",
"typescript": "^5.0.0",
"vitest": "^4.0.0"
}
}tsconfig.json:
{
"extends": "@repo/config/typescript/node.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}eslint.config.mjs:
import nodeConfig from "@repo/config/eslint/node";
export default [...nodeConfig];Example utility:
// src/formatDate.ts
/**
* Formats a date for display.
*
* @param date - The date to format
* @param locale - The locale to use (default: "en-US")
* @returns Formatted date string
*
* @example
* ```ts
* formatDate(new Date("2024-01-15")); // "Jan 15, 2024"
* formatDate(new Date("2024-01-15"), "de-DE"); // "15. Jan. 2024"
* ```
*/
export function formatDate(date: Date, locale = "en-US"): string {
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "short",
day: "numeric"
}).format(date);
}Export from index:
// src/index.ts
export { formatDate } from "./formatDate";
export { validateEmail } from "./validateEmail";
export { truncate } from "./truncate";That's it! Your utility package is ready to use.
Configuration package
For shared configs (ESLint, TypeScript, etc.):
{
"name": "@repo/my-config",
"version": "0.0.0",
"private": true,
"exports": {
"./eslint": "./eslint/index.js",
"./typescript/base": "./typescript/base.json",
"./typescript/react": "./typescript/react.json"
},
"dependencies": {
"eslint": "^9.0.0",
"typescript": "^5.0.0"
}
}Directory structure:
packages/my-config/
├── package.json
├── eslint/
│ └── index.js
└── typescript/
├── base.json
└── react.jsonESLint config:
// eslint/index.js
import js from "@eslint/js";
import tsPlugin from "@typescript-eslint/eslint-plugin";
export default [
js.configs.recommended,
{
plugins: {
"@typescript-eslint": tsPlugin
},
rules: {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "warn"
}
}
];TypeScript config:
// typescript/base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true
}
}That's it! Your config package is ready to use.
Using packages in apps
Add package dependency
# Add to a specific app
pnpm --filter=web add @repo/my-package
# Add to multiple apps
pnpm --filter=web --filter=mobile add @repo/my-packagepackage.json will update:
{
"dependencies": {
"@repo/my-package": "workspace:*"
}
}The workspace:* protocol:
- Tells pnpm to use the local package (not npm)
- Automatically updates when package changes
- Resolves to actual version in published packages
Import and use
// apps/web/src/app/page.tsx
import { Button } from "@repo/my-ui";
import { formatDate } from "@repo/my-utils";
import type { User } from "@repo/types";
export default function Page(): JSX.Element {
const today = formatDate(new Date());
return (
<div>
<h1>Today is {today}</h1>
<Button variant="primary" size="md">
Click me
</Button>
);
}That's it! TypeScript will auto-complete exports from your package.
Testing packages
Add Vitest
pnpm --filter=@repo/my-package add -D vitestvitest.config.ts:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["src/**/*.test.ts"],
coverage: {
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "dist/"]
}
}
});Add test script:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}Write tests
// src/formatDate.test.ts
import { describe, it, expect } from "vitest";
import { formatDate } from "./formatDate";
describe("formatDate", () => {
it("formats dates in US locale by default", () => {
const date = new Date("2024-01-15");
expect(formatDate(date)).toBe("Jan 15, 2024");
});
it("formats dates in German locale", () => {
const date = new Date("2024-01-15");
expect(formatDate(date, "de-DE")).toBe("15. Jan. 2024");
});
it("handles invalid dates gracefully", () => {
const date = new Date("invalid");
expect(formatDate(date)).toBe("Invalid Date");
});
});Run tests:
# Run tests for one package
pnpm --filter=@repo/my-package test
# Run tests for all packages
pnpm --filter="@repo/*" test
# Watch mode during development
pnpm --filter=@repo/my-package test:watchThat's it! Your package has comprehensive tests.
Team packages
Create team-specific package
For packages used only by one team:
# Create in team workspace
mkdir -p teams/ai/packages/my-ai-package/srcpackage.json:
{
"name": "@ai/my-ai-package",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"@repo/utils": "workspace:*"
},
"devDependencies": {
"@repo/config": "workspace:*",
"typescript": "^5.0.0"
}
}Team packages can:
- Use shared
@repo/*packages - Be used by team apps (
teams/ai/apps/*) - Cannot be used by other teams (unless promoted to
packages/)
When to promote to shared:
If multiple teams need the package, move it to packages/ and rename:
# Move package
mv teams/ai/packages/my-ai-package packages/my-ai-package
# Update package.json name
# "@ai/my-ai-package" → "@repo/my-ai-package"
# Update all imports in team apps
# "@ai/my-ai-package" → "@repo/my-ai-package"Versioning with Changesets
Create changeset
When you make changes to a package:
pnpm changesetYou'll be prompted:
- Which packages changed? — Select with spacebar, confirm with Enter
- What type of change? — patch (bug fix), minor (feature), major (breaking change)
- Describe the change — User-facing summary
Example:
🦋 Which packages would you like to include?
› ◉ @repo/my-package
◯ @repo/other-package
🦋 What kind of change is this for @repo/my-package?
› patch (bug fix)
minor (new feature)
major (breaking change)
🦋 Please enter a summary for this change
› Fix date formatting for German localeThis creates:
# .changeset/random-name.md
---
## "@repo/my-package": patch
Fix date formatting for German localeCommit the changeset:
git add .changeset/random-name.md
git commit -m "chore: add changeset for date formatting fix"Release process
When your PR is merged to main:
- Changesets bot opens a "Version Packages" PR
- Versions are bumped in package.json files
- CHANGELOG.md is updated with your summary
- Merge the PR to publish changes
That's it! Changesets handles versioning automatically.
Best practices
1. Keep packages focused
✅ Good package names:
- @repo/auth → Authentication only
- @repo/forms → Form components only
- @repo/analytics → Analytics utilities only
- @repo/date-utils → Date formatting only
❌ Bad package names:
- @repo/common → Too broad
- @repo/helpers → Too vague
- @repo/stuff → Meaningless
- @repo/utils → Use for truly generic utilitiesWhy? Focused packages are easier to test, version, and maintain.
2. Export types separately
// ✅ Good - allows type-only imports
export { Button } from "./Button";
export type { ButtonProps } from "./Button";
// In consuming code
import { Button } from "@repo/ui";
import type { ButtonProps } from "@repo/ui"; // Tree-shakeableWhy? Type-only imports don't add runtime code, reducing bundle size.
3. Use peer dependencies for React
{
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0"
}
}Why? Prevents multiple React versions in your app (causes bugs).
4. Document public API
/**
* Formats a date for display.
*
* @param date - The date to format
* @param locale - The locale to use (default: "en-US")
* @returns Formatted date string
*
* @example
* ```ts
* formatDate(new Date("2024-01-15")); // "Jan 15, 2024"
* formatDate(new Date("2024-01-15"), "de-DE"); // "15. Jan. 2024"
* ```
*/
export function formatDate(date: Date, locale = "en-US"): string {
// Implementation
}Why? JSDoc provides inline documentation in your IDE.
5. Avoid circular dependencies
❌ Bad - circular dependency:
@repo/auth → @repo/db → @repo/auth
✅ Good - linear dependency:
@repo/auth → @repo/db → @repo/typesHow to check:
# Install madge
pnpm add -D -w madge
# Check for circular dependencies
pnpm madge --circular packages/Why? Circular dependencies cause build errors and runtime bugs.
6. Keep dependencies minimal
{
"dependencies": {
// ✅ Only direct dependencies
"zod": "^4.0.0"
},
"devDependencies": {
// ✅ Build tools and types
"@repo/config": "workspace:*",
"typescript": "^5.0.0"
},
"peerDependencies": {
// ✅ Framework dependencies (React, Next.js)
"react": "^19.0.0"
}
}Why? Fewer dependencies mean faster installs, smaller bundles, fewer vulnerabilities.
Common patterns
Re-exporting from packages
// packages/ui/src/index.ts
// Re-export from sub-packages
export * from "./components/Button";
export * from "./components/Card";
// Re-export types separately
export type { ButtonProps } from "./components/Button";
export type { CardProps } from "./components/Card";
// Grouped exports
export * as utils from "./utils";
export * as hooks from "./hooks";Conditional exports
{
"exports": {
".": {
"import": "./src/index.ts",
"require": "./dist/index.js",
"types": "./src/index.ts"
},
"./client": {
"import": "./src/client/index.ts",
"types": "./src/client/index.ts"
},
"./server": {
"import": "./src/server/index.ts",
"types": "./src/server/index.ts"
}
}
}Usage:
import { Button } from "@repo/ui"; // Main export
import { useAuth } from "@repo/auth/client"; // Client-only
import { getSession } from "@repo/auth/server"; // Server-onlyBarrel files for organization
// src/components/index.ts (barrel file)
export { Button } from "./Button";
export { Card } from "./Card";
export { Input } from "./Input";
export type { ButtonProps, CardProps, InputProps } from "./types";
// Apps import from barrel
import { Button, Card } from "@repo/ui/components";Why? Cleaner imports, easier refactoring.
Troubleshooting
Package not found
Error:
Cannot find module '@repo/my-package' or its corresponding type declarationsSolution:
# 1. Verify package is installed
pnpm --filter=web ls @repo/my-package
# 2. If not installed, add it
pnpm --filter=web add @repo/my-package
# 3. Reinstall all dependencies
pnpm install
# 4. Restart TypeScript server in your editor
# VSCode: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server"Circular dependency error
Error:
Circular dependency detected: @repo/auth → @repo/db → @repo/authSolution:
# 1. Identify circular dependencies
pnpm madge --circular packages/
# 2. Extract shared types to @repo/types
# Move shared interfaces to packages/types/src/
# 3. Update imports
# @repo/auth → @repo/types
# @repo/db → @repo/typesType errors after adding package
Error:
Type 'ButtonProps' is not assignable to type 'IntrinsicAttributes & ButtonProps'Solution:
# 1. Check TypeScript config extends shared config
# tsconfig.json should have:
# "extends": "@repo/config/typescript/react.json"
# 2. Verify React versions match
pnpm ls react
# 3. Clear TypeScript cache
rm -rf node_modules/.cache
pnpm installBuild errors with workspace:*
Error:
Invalid version range: workspace:*Solution:
# 1. Verify pnpm version (requires 8.0+)
pnpm --version
# 2. Update pnpm if needed
npm install -g pnpm@latest
# 3. Verify .npmrc has:
# auto-install-peers=trueNext steps
- Explore existing packages: Shared Packages →
- Learn team workspaces: Team Workspaces →
- Understand architecture: Workspace Architecture →
- Follow conventions: Coding Conventions →
- Add tests: Testing & QA →
For Developers: Advanced package development
Advanced package configuration
Multi-entry packages
For packages with multiple entry points:
{
"name": "@repo/analytics",
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./server": "./src/server/index.ts",
"./react": "./src/react/index.tsx"
}
}TypeScript paths (in consuming apps):
{
"compilerOptions": {
"paths": {
"@repo/analytics": ["../../packages/analytics/src/index.ts"],
"@repo/analytics/client": ["../../packages/analytics/src/client/index.ts"],
"@repo/analytics/server": ["../../packages/analytics/src/server/index.ts"],
"@repo/analytics/react": ["../../packages/analytics/src/react/index.tsx"]
}
}
}Conditional exports by environment
{
"exports": {
".": {
"node": "./src/node.ts",
"browser": "./src/browser.ts",
"default": "./src/index.ts"
}
}
}How it works:
- Node.js imports
./src/node.ts - Browsers import
./src/browser.ts - Other environments import
./src/index.ts
Build packages (optional)
Most packages use raw TypeScript (no build step). For packages that need compilation, use tsdown with shared presets
from @repo/config/tsdown.
Internal packages (Node.js)
For packages used only within the monorepo:
tsdown.config.mjs:
import { node } from "@repo/config/tsdown";
export default {
...node,
entry: ["src/index.ts"],
external: [...node.external, "vitest"] // Add package-specific externals
};package.json:
{
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch"
},
"devDependencies": {
"tsdown": "catalog:"
}
}Available presets: node, browser, react, client - see platform/packages/config/tsdown/
Distribution packages (@oneapp/*)
For packages published to GitHub Packages (e.g., @repo/auth → @oneapp/auth):
tsdown.config.mjs:
import { createDistConfig } from "@repo/config/tsdown";
export default createDistConfig(
"node",
{
index: "src/index.ts",
client: "src/client.ts"
},
{
external: ["better-auth", "zod"] // Framework externals
}
);Examples: See platform/packages/auth/tsdown.config.mjs and platform/packages/uni-ui/tsdown.config.mjs
When to build:
- Package is published externally (npm/GitHub Packages)
- Need ESM distribution with type declarations
- Bundle internal
@repo/*dependencies
When NOT to build:
- Internal packages only (use raw TypeScript with
workspace:*) - Faster development (no build step)
- Simpler debugging (direct source access)
Package generators
Create packages faster with generators:
# Using Plop (example)
pnpm add -D -w plop
# Create generator
# plopfile.js
export default function (plop) {
plop.setGenerator('package', {
description: 'Create a new package',
prompts: [
{
type: 'input',
name: 'name',
message: 'Package name (without @repo/):',
},
{
type: 'list',
name: 'type',
message: 'Package type:',
choices: ['react', 'node', 'config'],
},
],
actions: [
{
type: 'addMany',
destination: 'packages/{{name}}',
base: 'templates/package-{{type}}',
templateFiles: 'templates/package-{{type}}/**/*',
},
],
});
}
# Generate package
pnpm plop packagePackage documentation
For complex packages, add comprehensive README:
# @repo/my-package
Brief description of what the package does.
## Installation
\`\`\`bash pnpm add @repo/my-package \`\`\`
## Usage
\`\`\`typescript import { myFunction } from "@repo/my-package";
const result = myFunction(); \`\`\`
## API Reference
### myFunction(param)
Description of function.
- **param** (Type) - Description
- **Returns** - Description
## Examples
### Basic usage
\`\`\`typescript // Example code \`\`\`
### Advanced usage
\`\`\`typescript // Advanced example \`\`\`
## Development
\`\`\`bash
# Run tests
pnpm test
# Type check
pnpm typecheck
# Lint
pnpm lint \`\`\`Internal package dependencies
When packages depend on each other:
// packages/auth/package.json
{
"dependencies": {
"@repo/db-prisma": "workspace:*",
"@repo/types": "workspace:*",
"better-auth": "^1.0.0"
}
}Dependency graph example:
@repo/auth
└─ @repo/db-prisma
└─ @repo/types
@repo/api
├─ @repo/auth
└─ @repo/typesBest practices:
- Keep dependency trees shallow (avoid deep nesting)
- Extract shared types to
@repo/types - Avoid circular dependencies
Package versioning strategy
Semantic versioning:
- patch (0.0.x) — Bug fixes, no API changes
- minor (0.x.0) — New features, backward compatible
- major (x.0.0) — Breaking changes
Examples:
# Bug fix (0.1.0 → 0.1.1)
pnpm changeset
# Select: patch
# Summary: "Fix email validation regex"
# New feature (0.1.1 → 0.2.0)
pnpm changeset
# Select: minor
# Summary: "Add phone number validation"
# Breaking change (0.2.0 → 1.0.0)
pnpm changeset
# Select: major
# Summary: "Remove deprecated validateUser function"Monorepo package best practices
1. Shared configurations
Use @repo/config for all config:
// packages/my-package/eslint.config.mjs
import reactConfig from "@repo/config/eslint/react";
export default [...reactConfig];2. Shared TypeScript types
Use @repo/types for shared types:
// packages/types/src/index.ts
export type { UserId, User } from "./user";
export type { ProductId, Product } from "./product";
export type { AsyncResult } from "./async-result";3. Internal package structure
packages/
├── types/ # Shared TypeScript types
├── utils/ # Generic utilities (no dependencies)
├── config/ # Shared configs (ESLint, TypeScript)
├── ui/ # UI components (depends on types)
├── auth/ # Authentication (depends on db, types)
├── db-prisma/ # Database client (depends on types)
└── api/ # API client (depends on types)Dependency order (bottom to top):
types(no dependencies)utils,config(no dependencies)ui,db-prisma(depend on types)auth(depends on db-prisma, types)api(depends on auth, types)
Performance optimization
1. Lazy loading for large packages
// Instead of direct import
import { HeavyComponent } from "@repo/ui";
// Use dynamic import
const HeavyComponent = lazy(() => import("@repo/ui/HeavyComponent"));2. Code splitting by entry point
{
"exports": {
"./Button": "./src/components/Button.tsx",
"./Card": "./src/components/Card.tsx"
}
}Apps can import specific components:
// Only imports Button code (not entire package)
import { Button } from "@repo/ui/Button";3. Tree-shaking friendly exports
// ✅ Good - tree-shakeable
export { formatDate } from "./formatDate";
export { formatCurrency } from "./formatCurrency";
// ❌ Bad - imports everything
export * from "./utils";Testing strategies
Unit tests for utilities:
// packages/utils/src/formatDate.test.ts
import { describe, it, expect } from "vitest";
import { formatDate } from "./formatDate";
describe("formatDate", () => {
it("formats US dates correctly", () => {
const date = new Date("2024-01-15");
expect(formatDate(date)).toBe("Jan 15, 2024");
});
});Component tests for UI packages:
// packages/ui/src/Button.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { Button } from "./Button";
describe("Button", () => {
it("renders children correctly", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("calls onClick when clicked", () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
screen.getByText("Click me").click();
expect(onClick).toHaveBeenCalledTimes(1);
});
});Integration tests across packages:
// packages/auth/src/auth.test.ts
import { describe, it, expect } from "vitest";
import { createUser } from "./createUser";
import { db } from "@repo/db-prisma";
describe("createUser", () => {
it("creates user in database", async () => {
const result = await createUser({ email: "test@example.com" });
expect(result.success).toBe(true);
if (result.success) {
const user = await db.user.findUnique({
where: { email: "test@example.com" }
});
expect(user).toBeDefined();
}
});
});Publishing packages to npm
For packages published to npm (not common in monorepos):
1. Update package.json:
{
"name": "@your-org/package-name",
"version": "1.0.0",
"private": false,
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/your-org/monorepo.git",
"directory": "packages/my-package"
},
"files": ["dist", "README.md", "LICENSE"]
}2. Build before publishing:
{
"scripts": {
"prepublishOnly": "pnpm build && pnpm test"
}
}3. Use Changesets for publishing:
# Create changeset
pnpm changeset
# Version packages
pnpm changeset version
# Publish to npm
pnpm changeset publishRelated documentation
- Shared Packages: All Packages →
- Team Workspaces: Team Workspaces Guide →
- Workspace Architecture: Architecture Overview →
- Dependencies: Dependency Management →
- Type Safety: Type Safety Patterns →
- Testing: Testing & QA Guide →
Error Handling
Complete guide to type-safe error handling patterns in the OneApp monorepo using AsyncResult, error boundaries, and validation.
Team Workspaces
Set up isolated team development environments in the OneApp monorepo — from directory structure to dependency management, with complete configuration examples.