# Adding a New Library Package

How to add a new shared library package to the `packages/` directory. Library packages are consumed by apps and other packages via pnpm workspace dependencies.

## Steps

1. **Create the package directory** under `packages/<name>/` with the following structure:

   ```
   packages/<name>/
     src/
       index.ts          # public API barrel export
     package.json
     tsconfig.json
     tsdown.config.ts    # build config
     vitest.config.ts    # test config
   ```

2. **Configure `package.json`.** Use the `@coda/<name>` naming convention. Key fields:

   ```json
   {
     "name": "@coda/<name>",
     "private": true,
     "version": "0.0.0",
     "type": "module",
     "sideEffects": false,
     "engines": { "node": ">=24.0.0" },
     "exports": {
       ".": {
         "types": "./dist/index.d.mts",
         "default": "./dist/index.mjs"
       }
     },
     "files": ["dist", "README.md"],
     "scripts": {
       "build": "tsdown",
       "dev": "tsdown --watch",
       "typecheck": "tsc --noEmit",
       "lint": "eslint src/",
       "test": "pnpm lint && pnpm test:unit --coverage",
       "test:unit": "vitest run --passWithNoTests"
     }
   }
   ```

3. **Set up TypeScript.** Create a `tsconfig.json` that extends the library base:

   ```json
   {
     "extends": "../../tsconfig.lib.json",
     "include": ["src"],
     "exclude": ["src/**/__tests__"]
   }
   ```

   If the package uses `enum` syntax, add `"compilerOptions": { "erasableSyntaxOnly": false }`. If the package includes proto-generated code in `gen/`, also add `"gen/**/*"` to `include`.

4. **Add test and lint config files.**

   **`tsconfig.test.json`**:

   ```json
   {
     "extends": "./tsconfig.json",
     "compilerOptions": { "types": ["vitest/globals", "node"] },
     "include": ["src"],
     "exclude": []
   }
   ```

   **`vitest.config.ts`** — Vitest is the standard test runner (not Jest):

   ```ts
   import { defineConfig } from "vitest/config";
   export default defineConfig({
     test: {
       environment: "node",
       include: ["src/**/__tests__/**/*.test.ts"],
       globals: true,
     },
   });
   ```

   **`eslint.config.mjs`**:

   ```js
   import { defineConfig, globalIgnores } from "eslint/config";
   import {
     baseExtends,
     baseRules,
     testRules,
     eslintConfigPrettier,
   } from "../../eslint.base.mjs";

   export default defineConfig([
     globalIgnores(["dist"]),
     {
       files: ["src/**/*.ts"],
       extends: baseExtends,
       languageOptions: {
         parserOptions: {
           project: "./tsconfig.json",
           tsconfigRootDir: import.meta.dirname,
         },
       },
       rules: baseRules,
     },
     {
       files: ["src/**/__tests__/**/*.ts", "src/**/*.test.ts"],
       languageOptions: {
         parserOptions: {
           project: "./tsconfig.test.json",
           tsconfigRootDir: import.meta.dirname,
         },
       },
       rules: testRules,
     },
     eslintConfigPrettier,
   ]);
   ```

   **`.prettierignore`**: `dist` (add `gen` if the package has proto-generated code).

   **`tsdown.config.ts`** — copy from an existing package like `packages/core-api/`.

5. **Update the Dockerfile.** The `deps` stage auto-discovers packages via `COPY --parents` — **no change needed** for the manifest. In the `dev-deps` stage, add COPY blocks for your source and configs, placed in the correct **build tier**:
   - **Tier 0** (no `@coda/*` dependencies): add before the tier 0 `RUN pnpm --filter ... build`
   - **Tier 1** (depends on tier-0 packages): add before the tier 1 `RUN pnpm --filter ... build`

   ```dockerfile
   # <name> source + configs
   COPY  --chown=node:node packages/<name>/tsconfig.json packages/<name>/tsdown.config.ts packages/<name>/eslint.config.mjs packages/<name>/.prettierignore ./packages/<name>/
   COPY  --chown=node:node packages/<name>/src ./packages/<name>/src
   ```

   Add the package to the appropriate tier's `RUN pnpm --filter` line. The `test-base` stage auto-discovers vitest configs via `COPY --parents` globs — **no change needed**.

6. **Register in the workspace.** The `pnpm-workspace.yaml` already includes `packages/*`, so new directories are picked up automatically. Run `pnpm install` to link the workspace.

7. **Add to `build:libs`.** If the package needs to be built before typecheck (because other packages import its types), add it to the `build:libs` script in the root `package.json`.

8. **Consume from other packages.** Add a workspace dependency in the consumer's `package.json`:

   ```json
   {
     "dependencies": {
       "@coda/<name>": "workspace:*"
     }
   }
   ```

9. **Validate.**

   ```bash
   pnpm install
   pnpm --filter @coda/<name> typecheck
   pnpm --filter @coda/<name> lint
   pnpm --filter @coda/<name> test:unit
   bash scripts/validate-workspace-sync.sh   # should pass
   ```

10. **Add tests and a README.**

## Conventions

- **ESM output** -- shared packages use ESM via tsdown (dual ESM/CJS when needed)
- **Barrel exports** -- re-export the public API from `src/index.ts`
- **No side effects** -- set `"sideEffects": false` so bundlers can tree-shake
- **Shared utilities belong in `@coda/common`** -- only create a new package when the concern is distinct enough to warrant its own boundary (e.g., proto definitions, database client, sandbox engine)

## See also

- [CONTRIBUTING.md](../../CONTRIBUTING.md) for the full contribution workflow, PR process, and code conventions
