# Shared Package Extraction

> Identifying duplicated code across repos and extracting it into reusable shared packages. Turn 1,856 lines of duplication into 275 lines of shared library.

## When to Use

- Same utility code appears in 3+ places
- Multiple services/dashboards share identical patterns (DB client, auth, errors)
- You're copy-pasting code between repos
- A bug fix needs to be applied in multiple places

## The Pattern

```
Before: Each consumer has its own copy
┌─────────┐  ┌─────────┐  ┌─────────┐
│ Service A│  │ Service B│  │Dashboard │
│ cosmos.ts│  │ cosmos.ts│  │ cosmos.ts│  ← 3 copies, drift over time
│ auth.ts  │  │ auth.ts  │  │ auth.ts  │
│ errors.ts│  │ errors.ts│  │ errors.ts│
└─────────┘  └─────────┘  └─────────┘

After: Shared packages consumed by all
┌──────────────────────────────────────┐
│ packages/                            │
│  ├── cosmos/   → @scope/cosmos       │
│  ├── auth/     → @scope/auth         │
│  └── errors/   → @scope/errors       │
└──────────────────┬───────────────────┘
       ┌───────────┼───────────┐
┌──────▼──┐ ┌─────▼───┐ ┌────▼─────┐
│ Service A│ │ Service B│ │Dashboard │
└─────────┘ └─────────┘ └──────────┘
```

## Step-by-Step

### 1. Identify duplication

Ask the agent to scan for patterns:

```
Scan these directories for duplicated code patterns:
- service-a/src/lib/
- service-b/src/lib/
- dashboard/src/lib/

List files that appear in 2+ places with similar content.
Report: filename, line count, similarity %.
```

Common duplications found in real projects:

| Package                          | Consumers | Duplicated LOC |
| -------------------------------- | --------- | -------------- |
| errors (typed HTTP errors)       | 6         | ~157           |
| cosmos (DB client singleton)     | 6         | ~200           |
| config (env loader + product ID) | 4         | ~180           |
| auth (JWT + password hashing)    | 5         | ~250           |
| api-client (fetch wrapper)       | 5         | ~150           |

### 2. Extract the package

Create a new package in the shared workspace:

```
packages/errors/
├── src/
│   └── index.ts    ← All exports
├── package.json
└── tsconfig.json
```

**package.json:**

```json
{
  "name": "@scope/errors",
  "version": "0.1.0",
  "type": "module",
  "exports": {
    ".": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^5.7.0"
  }
}
```

**tsconfig.json:**

```json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}
```

### 3. Migrate consumers

Replace local copies with the shared package:

```diff
// service-a/package.json
{
  "dependencies": {
-   // (inline cosmos.ts, errors.ts)
+   "@scope/errors": "workspace:*",
+   "@scope/cosmos": "workspace:*"
  }
}
```

```diff
// service-a/src/lib/errors.ts
- export class BadRequestError extends Error { ... }
- export class NotFoundError extends Error { ... }
+ export { BadRequestError, NotFoundError } from '@scope/errors';
```

### 4. Verify all consumers

```bash
# Build shared packages first
pnpm build --filter '@scope/*'

# Then verify each consumer
pnpm build --filter 'service-a'
pnpm build --filter 'service-b'
pnpm build --filter 'dashboard'
```

## Package Design Guidelines

### Heavy deps go in peerDependencies

```json
{
  "peerDependencies": {
    "@azure/cosmos": "^4.0.0",
    "jose": "^5.0.0",
    "zod": "^3.0.0"
  }
}
```

This prevents version conflicts and reduces bundle size.

### Workspace references

In a pnpm workspace:

```json
{ "@scope/errors": "workspace:*" }
```

For consumers in separate repos (via `file:` refs):

```json
{ "@scope/errors": "file:../common-platform/packages/errors" }
```

**Important:** Run `pnpm build` in the shared repo before `npm install` in consuming repos.

### Self-contained config per service

Don't put Zod config schemas in shared packages (Zod version mismatch risk). Each service should have its own `src/lib/config.ts`:

```typescript
// service-a/src/lib/config.ts — Self-contained
import { z } from 'zod';

const configSchema = z.object({
  PORT: z.coerce.number().default(4003),
  COSMOS_ENDPOINT: z.string(),
  COSMOS_KEY: z.string(),
  JWT_SECRET: z.string(),
});

export const config = configSchema.parse(process.env);
```

### Re-export pattern

Services re-export from shared packages in their `src/lib/` files:

```typescript
// service-a/src/lib/errors.ts
export { BadRequestError, NotFoundError, ConflictError } from '@scope/errors';

// service-a/src/lib/cosmos.ts
export { getCosmosClient, getContainer } from '@scope/cosmos';
```

This provides a clean internal import path and makes swapping implementations easy.

## Priority Framework

| Priority | Criteria                                     | Example                                  |
| -------- | -------------------------------------------- | ---------------------------------------- |
| P0       | 5+ consumers, bugs require multi-repo fixes  | errors, cosmos client                    |
| P1       | 3-4 consumers, moderate complexity           | config loader, auth/JWT                  |
| P2       | 2-3 consumers, some variation between copies | API client, React auth                   |
| P3       | Cross-platform generation needed             | design tokens (JSON→CSS/TS/Kotlin/Swift) |

## Anti-Patterns

- **Extracting too early** — Wait until you have 3+ consumers before extracting
- **Shared config schemas** — Zod version mismatch across packages causes runtime errors
- **Breaking changes without versioning** — Use semantic versioning or workspace refs
- **Putting everything in one mega-package** — Split by domain (errors, cosmos, auth)
- **Not building before consuming** — `file:` refs need built dist/ to work

## Related Skills

- [13 — Module Pattern](./13-module-pattern.md)
- [15 — Service Consolidation](./15-service-consolidation.md)
- [17 — Multi-Repo Coordination](./17-multi-repo-coordination.md)
