OneApp Docs
Guides

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 configuration

2. 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-package

That'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 true for 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.json

ESLint 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-package

package.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 vitest

vitest.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:watch

That'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/src

package.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 changeset

You'll be prompted:

  1. Which packages changed? — Select with spacebar, confirm with Enter
  2. What type of change? — patch (bug fix), minor (feature), major (breaking change)
  3. 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 locale

This creates:

# .changeset/random-name.md

---

## "@repo/my-package": patch

Fix date formatting for German locale

Commit 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:

  1. Changesets bot opens a "Version Packages" PR
  2. Versions are bumped in package.json files
  3. CHANGELOG.md is updated with your summary
  4. 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 utilities

Why? 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-shakeable

Why? 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/types

How 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-only

Barrel 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 declarations

Solution:

# 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/auth

Solution:

# 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/types

Type 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 install

Build 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=true

Next steps

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 package

Package 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/types

Best 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):

  1. types (no dependencies)
  2. utils, config (no dependencies)
  3. ui, db-prisma (depend on types)
  4. auth (depends on db-prisma, types)
  5. 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 publish

On this page